• 【题解】夕张的改造


    题意

    给定一棵树,每次可以删掉一条边在加上另一条边,使它仍是一棵树,求操作至多(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 inom{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
    ", res);
    	return 0;
    }
    
  • 相关阅读:
    IOS开发环境
    IOS开发环境搭建
    Eclipse简明使用教程(java集成开发环境)
    分布式相关
    成为架构师之路认识分布式架构
    什么是分布式系统,如何学习分布式系统
    分布式定义
    VIM命令详解
    vim常用命令
    vi/vim 命令使用详解
  • 原文地址:https://www.cnblogs.com/RiverHamster/p/sol-oj4042.html
Copyright © 2020-2023  润新知