题解:[LBA-OI R1 C] 策定乾坤

题目传送门

精妙的 DP。

题意分析

钦定有向边 \(i\rightarrow j\) 表示 \(i\) 打败了 \(j\)

题意即给定 \(m\) 个环,第 \(i\) 个环有 \(a_i\) 个点,\(m-1\) 条边连接了 \(m\) 个环,求删边使其成为基环树森林的方案数。

显然考虑 DP。

容易发现 \(m\) 个环由 \(m-1\) 条边连接,换而言之,这 \(m\) 个环形成了一棵树,考虑树形 DP,得到子树信息后用环上 DP 合并子树信息。

考虑删边操作的影响:

  • 删掉环里的边,环变成一条链。
  • 删掉环之间的边,变成两个连通块。

同时基环树又涉及到环是否存在,把环丢入状态,设 \(f_{x,0},f_{x,1}\) 分别表示环 \(x\) 所在的连通块无环、有环的方案数。注意,为了满足基环树森林的限制,下文中「有环」的状态里,一个连通块只能有至多 \(1\) 个环。

当环 \(x\) 是树上的叶节点的时候,则有:

\[\begin{aligned} f_{x,0}&=a_x\\ f_{x,1}&=1 \end{aligned} \]

之后考虑非叶节点如何环上 DP 合并子树信息。

显然一个环上只有连接了其他环的边才是重要的。为了方便,对环节点重编号,记连接当前环的父环的环节点为 \(0\) 号环节点,其余有效环节点编号分别为 \(1,2,\cdots,s\)。环节点 \(i\) 原来的编号记为 \(\textit{id}_i\)

考虑如何求解 \(f_{x,0},f_{x,1}\)。因为环 \(x\) 是可以断的,同时状态里的环还可以包括子树里的环,因此考虑把断环后,环 \(x\) 的第一个连通块和其余连通块分开考虑。

\(g_{v,i\in\set{0/1},j\in\set{0/1/2}}\) 表示 \(x\) 合并到环节点 \(v\),环上第一个连通块是否有环,\(v\) 所在的连通块是否有环(或 \(v\) 在第一个连通块):

  • \(g_{v,0,0}\):环上第一个连通块无环,\(v\) 所在连通块无环。
  • \(g_{v,1,0}\):环上第一个连通块有环,\(v\) 所在连通块无环。
  • \(g_{v,0,1}\):环上第一个连通块无环,\(v\) 所在连通块有环。
  • \(g_{v,1,1}\):环上第一个连通块有环,\(v\) 所在连通块有环。
  • \(g_{v,0,2}\):环上第一个连通块无环,\(v\) 处于环上第一个连通块。
  • \(g_{v,1,2}\):环上第一个连通块有环,\(v\) 处于环上第一个连通块。

\(d=a_x-\textit{id}_s\) 表示 \(s\)\(0\) 之间的实际边数。得到 \(g_s\) 后,再考虑断环 \(x\) 就是断掉 \(s\)\(0\) 之间的边,共有 \(d\) 种选择。

  • 对于 \(f_{x,0}\),贡献有:

    • \(g_{s,0,0}\):第一个连通块和 \(s\) 所在连通块都没有环,不断掉环 \(x\),保留这两个连通块。
    • \(d\cdot g_{s,0,1}\):断掉环 \(x\),并丢掉 \(g_{s,0,1}\)\(s\) 所在连通块的环。
    • \(d\cdot g_{s,0,2}\):断掉环 \(x\),从而满足没有环的状态限制。
  • 对于 \(f_{x,1}\),贡献有:

    • \(g_{s,0,1}\):保留 \(s\) 所在连通块内的环,连 \(s\)\(0\)。第一个连通块不能丢,否则其不满足基环树森林的限制。
    • \(g_{s,1,0}\):保留第一个连通块的环,还要保留 \(s\)\(0\) 的边,否则 \(s\) 的连通块没有环,不满足基环树森林的限制。
    • \(g_{s,0,2}\):不断掉环 \(x\)。环就是环 \(x\)
    • \(d\cdot g_{s,1,1}\):要把 \(s\) 所在连通块丢掉,断环 \(x\),否则会出现两个环。
    • \(d\cdot g_{s,1,2}\):要把环 \(x\) 断掉,否则会出现两个环。

整合一下,即:

\[\begin{aligned} f_{x,0}&=g_{s,0,0}+d(g_{s,0,1}+g_{s,0,2})\\ f_{x,1}&=g_{s,0,1}+g_{s,0,2}+g_{s,1,0}+d(g_{s,1,1}+g_{s,1,2}) \end{aligned} \]

如何从 \(g_{v-1}\) 转移得到 \(g_v\)

\(v'\) 表示 \(v\) 连接的另一个环的编号,\(f_0=f_{v',0},f_1=f_{v',1},d=\textit{id}_v-\textit{id}_{v-1}\)

首先发现,当 \(v\) 不在第一个连通块时,\(v\) 的环只与 \(v-1\) 有关,与第一个连通块无关。因此 \(g_{v,0,j},g_{v,1,j}\) 的转移式子是一样的。

  • \(g_{v,i,0}\)

    此时要求 \(v\) 所处连通块无环,且不是第一个连通块。

    \(v\) 的子树合并到 \(v\) 上有 \(f_0+f_1\) 种方案,如果子树有环就断开,无环就只能挂到 \(v\) 上,期望 \(v\) 连接的地方能够出现一个环。所以都要乘 \(f_0+f_1\)

    • 保留与 \(v-1\) 的无环连通块的 \(d\) 条边,共 \((f_0+f_1)g_{v-1,i,0}\)

    • 断开 \(v-1\sim v\)\(d\) 条边,\(d(f_0+f_1)(g_{v-1,i,1}+g_{v-1,i,2})\)

      考虑断边有 \(d\) 种,合并有 \(f_0+f_1\) 种。

      \(v-1\) 的状态可以是有环,因此断开。

      \(v-1\) 的状态可以是在第一个连通块,\(v-1\) 的连通块里的环可以在其他地方,也可以断开。

    \[g_{v,i,0}=(f_0+f_1)g_{v-1,i,0}+d(f_0+f_1)(g_{v-1,i,1}+g_{v-1,i,2}) \]

  • \(g_{v,i,1}\)

    此时要求 \(v\) 所在连通块有环,且不是第一个连通块。

    • \(g_{v-1,i,0}\)\(v-1\) 无环,环只能在挂在 \(v\) 上的子树上。贡献为 \(f_1g_{v-1,i,0}\)

    • \(g_{v-1,i,1}\)\(v-1\) 有环,分类讨论。

      如果 \(v\) 的环是 \(v-1\) 的环,保留 \(v-1\sim v\) 的边,一样的把 \(v\) 的子树合并到 \(v-1\) 上,有 \(f_0+f_1\) 种方案。

      如果 \(v\) 的环不是 \(v-1\) 的环,那就在 \(d\) 条边中断开,然后把 \(v\) 的子树的环引入,有 \(df_1\) 种方案。

    • \(g_{v-1,i,2}\)

      因为 \(v\) 不在第一个连通块,所以必须断开 \(d\) 条边中的一条,然后引入 \(v\) 的子树中的环。方案数 \(df_1\)

    \[g_{v,i,1}=f_1g_{v-1,i,0}+(f_0+f_1+df_1)g_{v-1,i,1}+df_1g_{v-1,i,2} \]

  • \(g_{v,0,2}\)

    此时要求 \(v\) 在第一个连通块,且无环。显然 \(v-1\) 的状态也必须是无环的。

    显然必须保留与 \(v-1\) 的连边,然后再把 \(v\) 的子树合并,有 \(f_0+f_1\) 种方案。

    \[g_{v,0,2}=(f_0+f_1)g_{v-1,0,2} \]

  • \(g_{v,1,2}\)

    分类讨论环的位置:

    • \(g_{v-1,0,2}\):环不从 \(v-1\) 来,那就只能从子树引入环,\(f_1\) 种方案。
    • \(g_{v-1,1,2}\):环从 \(v-1\) 来,合并 \(v\) 的子树即可,\(f_0+f_1\) 种方案。

    \[g_{v,1,2}=f_1g_{v-1,0,2}+(f_0+f_1)g_{v-1,1,2} \]

再考虑一下初始状态,令第一个点编号为 \(v_1\),记 \(d=\textit{id}_{v_1},f_0=f_{v_1',0},f_1=f_{v_1',1}\)

考虑如果有环,则一定只能从 \(v_1\) 的子树里引入,则有:

\[\begin{aligned} g_{0,0,0}&=d(f_0+f_1)\\ g_{0,0,1}&=df_1\\ g_{0,0,2}&=f_0+f_1\\ g_{0,1,0}&=0\\ g_{0,1,1}&=0\\ g_{0,1,2}&=f_1 \end{aligned} \]

因为要对环节点重编号后排序来 DP,时间复杂度 \(\mathcal O(n\log n)\)

AC 代码

//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
#define int long long
constexpr const int M=5e5,P=998244353;
struct edge{
	int v,i,j;
};
vector<edge>e[M+1];
int m,a[M+1],f[M+1][2];
void dfs(int x,int k){
	for(auto [v,i,j]:e[x]){
		dfs(v,j);
	}
	if(!e[x].size()){
		f[x][0]=a[x];
		f[x][1]=1;
		return;
	}
	vector<pair<int,int>>p;	
	for(auto [v,i,j]:e[x]){
		p.push_back({(i-k+a[x])%a[x],v});
	}
	sort(p.begin(),p.end());
	int f0=f[p[0].second][0],f1=f[p[0].second][1],d=p[0].first;
	static int g[M+1][2][3];
	g[0][0][0]=1ll*d*(f0+f1)%P;
	g[0][0][1]=1ll*d*f1%P;
	g[0][0][2]=(f0+f1)%P;
	g[0][1][0]=0;
	g[0][1][1]=0;
	g[0][1][2]=f1;
	for(int v=1;v<p.size();v++){
		f0=f[p[v].second][0],f1=f[p[v].second][1],d=(p[v].first-p[v-1].first)%P;
		g[v][0][0]=( 1ll*g[v-1][0][0]*(f0+f1)%P + 1ll*(g[v-1][0][1] + g[v-1][0][2])*d%P*(f0+f1)%P )%P;
		g[v][0][1]=( 1ll*g[v-1][0][0]*f1%P + 1ll*g[v-1][0][1]*(f0+f1+1ll*d*f1%P)%P + 1ll*g[v-1][0][2]*d%P*f1%P )%P;
		g[v][0][2]=1ll*g[v-1][0][2]*(f0+f1)%P;
		g[v][1][0]=( 1ll*g[v-1][1][0]*(f0+f1)%P + 1ll*(g[v-1][1][1] + g[v-1][1][2])*d%P*(f0+f1)%P )%P;
		g[v][1][1]=( 1ll*g[v-1][1][0]*f1%P + 1ll*g[v-1][1][1]*(f0+f1+1ll*d*f1%P)%P + 1ll*g[v-1][1][2]*d%P*f1%P )%P;
		g[v][1][2]=( 1ll*g[v-1][0][2]*f1%P + 1ll*g[v-1][1][2]*(f0+f1)%P )%P;
	}
	d=(a[x]-p.back().first)%P;
	f[x][0]=( g[p.size()-1][0][0] + 1ll*d*(g[p.size()-1][0][1] + g[p.size()-1][0][2])%P )%P;
	f[x][1]=( 1ll*g[p.size()-1][0][1] + g[p.size()-1][0][2] + g[p.size()-1][1][0] + 1ll*d*(g[p.size()-1][1][1]+g[p.size()-1][1][2])%P )%P;
}
main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>m;
	for(int i=1;i<=m;i++){
		cin>>a[i];
	}
	for(int u=2;u<=m;u++){
		int v,i,j;
		cin>>v>>i>>j;
		e[v].push_back({u,j,i});
	}
	dfs(1,1);
	int ans=f[1][1];
	if(ans<0){
		ans+=P;
	}
	cout<<ans<<'\n';
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}
posted @ 2026-07-11 08:40  TH911  阅读(7)  评论(0)    收藏  举报