【题解】夕张的改造

题意

给定一棵树,每次可以删掉一条边在加上另一条边,使它仍是一棵树,求操作至多\(k\)次可以得到的树的形态数。

判定形态不同:一条边\((x, y)\)在第一棵树中出现,在另一棵树中不出现。

\(0 \le k \le n \le 50, n \ge 1\)

思路

其实有一种 Matrix-Tree 定理+插值思路:见references部分,复杂度\(\mathcal O(n^4)\),不讲了。

结论

\(n\)个点的森林,含\(k\)个连通块,每个连通块的大小为\(a_i,\sum a = n\),则在这些连通块间连边的方案数:

\[f(n, a) = n^{k-2}\cdot \prod{a_i} \]

证明见references部分。

容斥

显然删去\(k\)条边会拆除\(k+1\)个连通块,可以代入计算,但这样有一个问题:可能会把一条删去的原树边连回去,并且可能被计算很多次(一个连通块有多种拆分)。

考虑容斥。限制即需要删掉\(k\)条边,且不能连回去。每种方案都要选中\(k\)条边,然后其中的一部分会被删去,这样算出恰好删掉\(k\)条边的方案。

转化一下。枚举被删掉的边的个数\(m\),则强制违反了\(k-m\)个限制,其他限制任意。然后这种方案会被若干种选择\(k\)条边的方案包含,因为还有\(k-m\)条没选,还有\(n-1-m\)条边可以选。写式子:

\[S(k) = (-1)^{k-m}\cdot \sum_{m=0}^k \binom{n-1-m}{k-m} F(m) \]

其中\(F(m)\)为在原树上删去\(m\)条边后再连\(m\)条边的方案数。

根据样例,应该把删\(0 \sim k\)的方案都算一遍加起来。

DP

\(F(m)\)直接 DP 就好了。\(n^{k-2}\)可以直接乘上,后面的求积可以考虑组合意义:每个连通块内任选一个点的方案数。设\(f(x, i, 0/1)\)为以\(x\)为根的子树,分成\(i\)个连通块,且根所在的块是否有关键点。分连边/不连边讨论即可。

复杂度

容斥、 DP 均为\(\mathcal O(n^2)\)

应该可以分治 NTT ,容斥乱搞优化到\(\mathcal O(n \log^2 n)\),但窝不会

References

关于两种做法的参考

结论的 prufer 序列证明

结论的 Matrix-Tree + 手玩证明

Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define File(s) freopen(s".in", "r", stdin), freopen(s".out", "w", stdout)

const int mod = 998244353;
inline int add(int x, int y){return x+y>=mod ? x+y-mod : x+y;}
inline int sub(int x, int y){return x-y<0 ? x-y+mod : x-y;}
inline int mul(int x, int y){return 1LL * x * y % mod;}
inline int power(int x, int y){
	int res = 1;
	for(; y; y>>=1, x = mul(x, x)) if(y & 1) res = mul(res, x);
	return res;
}
inline int inv(int x){return power(x, mod - 2);}
const int Node = 60;
vector<int> G[Node];
int f[Node][Node][2]; // i 为根,j 个连通块,根所在的连通块选了/没选
int n, k;
int C[Node][Node];

int dfs(int x){ // returns min(k + 1, size)
	f[x][1][0] = f[x][1][1] = 1;
	int s = 1;
	static int pf[Node][2];
	for(int v : G[x]){
		int sv = dfs(v);
		int ns = min(s + sv, k + 1);
		for(int i=1; i<=s; i++){
			pf[i][0] = f[x][i][0], pf[i][1] = f[x][i][1];
			f[x][i][0] = f[x][i][1] = 0;
		}
		for(int i=1; i<=s; i++)
			for(int j=1, lim=min(sv, ns - i + 1); j<=lim; j++){
				f[x][i+j][0] = add(f[x][i+j][0], mul(pf[i][0], f[v][j][1]));
				f[x][i+j][1] = add(f[x][i+j][1], mul(pf[i][1], f[v][j][1]));
				f[x][i+j-1][0] = add(f[x][i+j-1][0], mul(pf[i][0], f[v][j][0]));
				f[x][i+j-1][1] = add(f[x][i+j-1][1], add(mul(pf[i][0], f[v][j][1]), mul(pf[i][1], f[v][j][0])));
			}
		s = ns;
	}
	return s;
}

void preC(int n){
	C[0][0] = 1;
	for(int i=1; i<=n; i++){
		C[i][0] = 1;
		for(int j=1; j<=i; j++) C[i][j] = add(C[i-1][j], C[i-1][j-1]);
	}
}
int cal(int k){
	int res = C[n-1][k];
	if(k & 1) res = sub(0, res);
	for(int i=1; i<=k; i++){
		int F = mul(power(n, i - 1), f[0][i + 1][1]);
		if((k ^ i) & 1) res = sub(res, mul(F, C[n - 1 - i][k - i]));
		else res = add(res, mul(F, C[n - 1 - i][k - i]));
	}
	return res;
}

int main(){
	scanf("%d%d", &n, &k);
	for(int i=1; i<n; i++){
		int fa; scanf("%d", &fa);
		G[fa].push_back(i);
	}
	dfs(0);
	preC(n + 2);
	int res = 0;
	for(int i=0; i<=k; i++) res = add(res, cal(i));
	printf("%d\n", res);
	return 0;
}
posted @ 2020-03-01 14:01  RiverHamster  阅读(426)  评论(1编辑  收藏  举报
\