Codeforces 917D

Codeforces 题目传送门 & 洛谷题目传送门

刚好看到 wjz 在做这题,心想这题之前好像省选前做过,当时觉得是道挺不错的题,为啥没写题解呢?于是就过来补了,由此可见我真是个大鸽子((

跑题了跑题了……

这里提供两种解法:

Algorithm 1.

注意到“恰好”二字有点蓝瘦,因此套路地想到二项式反演,也就说我们钦定 (k) 条边必须与原树中的边重合,其余边可以随便连的方案数,我们假设这些与原树中的边重合的边构成的集合为 (E'),那么 (E') 中显然包含了一些连通块,而我们的任务就是在这些连通块之间连上一些边,使其成为一棵树,我们假设 (E') 中的点组成了 (r) 个连通块,大小分别为 (a_1,a_2,cdots,a_r),那么这里就有一个大家熟知,而我不知道的定理:构成的生成树个数为 (n^{r-2}prodlimits_{i=1}^ra_i)

证明:自己瞎 yy 的,证明错了不要打我(大雾

就是考虑 Prufer 序列,我们假设 (i) 在 Prufer 序列中出现的次数为 (p_i),那么可以列出柿子:

[egin{aligned} |T|&=sumlimits_{sum p_i=r-2}prodlimits_{i=1}^ra_i^{p_i+1} imesdbinom{r-2}{p_1,p_2,cdots,p_r}\ &=sumlimits_{sum p_i=r-2}(r-2)!prodlimits_{i=1}^rdfrac{a_i^{p_i+1}}{p_i!}\ &=prodlimits_{i=1}^ra_i imes(r-2)!sumlimits_{sum p_i=r-2}prod_{i=1}^rdfrac{a_i^{p_i}}{p_i!} end{aligned} ]

注意到后面那坨东西就是 (prodlimits_{i=1}^r[x^{p_i}]e^{a_i}),因此后面 (sum) 里面的东西可以改写为 (sumlimits_{sum p_i=n-2}prodlimits_{i=1}^r[x^{p_i}]e^{a_i}=[x^{n-2}]prodlimits_{i=1}^re^{a_i}=[x^{r-2}]e^n=n^{r-2}·dfrac{1}{(r-2)!}),故 (|T|=prodlimits_{i=1}^ra_i imes(r-2)! imes n^{r-2} imes(r-2)!=n^{r-2}prodlimits_{i=1}^ra_i)

大功告成。

注意到当我们枚举重合边数 (k) 时,(r=n-k),因此前面 (n^{r-2}) 是常量,我们只需考虑后面的东西即可,而后面的东西有着清晰的组合意义:将原树分成 (r) 个连通块,每个连通块里恰好放一个球的方案数,因此考虑树形 (dp)(dp_{u,j,0/1}) 表示 (u) 的子树内划分为 (j) 个连通块,(u) 所在的连通块是否放了球的方案数。按照树上背包的套路合并即可,具体来说,如果我们要合并以 (x,y) 为根的子树,其中 (x) 为父亲,那么:

  • (dp_{x,i+j,0}leftarrow dp_{x,i,0} imes dp_{y,j,1})
  • (dp_{x,i+j,1}leftarrow dp_{x,i,1} imes dp_{y,j,1})
  • (dp_{x,i+j-1,0}leftarrow dp_{x,i,0} imes dp_{y,j,0})
  • (dp_{x,i+j-1,1}leftarrow dp_{x,i,0} imes dp_{y,j,1}+dp_{x,i,1} imes dp_{y,j,0})

时间复杂度 (n^2)

const int MAXN=100;
const int MOD=1e9+7;
int qpow(int x,int e){
	if(e<0) e+=MOD-1;int ret=1;
	for(;e;e>>=1,x=1ll*x*x%MOD) if(e&1) ret=1ll*ret*x%MOD;
	return ret;
}
int fac[MAXN+5],ifac[MAXN+5];
void init_fac(int n){
	fac[0]=ifac[0]=ifac[1]=1;
	for(int i=2;i<=n;i++) ifac[i]=1ll*(MOD-MOD/i)*ifac[MOD%i]%MOD;
	for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*ifac[i]%MOD;
}
int binom(int x,int y){return 1ll*fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;}
int n,hd[MAXN+5],to[MAXN*2+5],nxt[MAXN*2+5],ec=0;
void adde(int u,int v){to[++ec]=v;nxt[ec]=hd[u];hd[u]=ec;}
int dp[MAXN+5][MAXN+5][2],siz[MAXN+5],tmp[MAXN+5][2];
int f[MAXN+5],g[MAXN+5];
void dfs(int x,int f){
	dp[x][1][0]=dp[x][1][1]=siz[x]=1;
	for(int e=hd[x];e;e=nxt[e]){
		int y=to[e];if(y==f) continue;dfs(y,x);memset(tmp,0,sizeof(tmp));
		for(int i=1;i<=siz[x];i++) for(int j=1;j<=siz[y];j++){
			tmp[i+j][0]=(tmp[i+j][0]+1ll*dp[x][i][0]*dp[y][j][1])%MOD;
			tmp[i+j][1]=(tmp[i+j][1]+1ll*dp[x][i][1]*dp[y][j][1])%MOD;
			tmp[i+j-1][0]=(tmp[i+j-1][0]+1ll*dp[x][i][0]*dp[y][j][0])%MOD;
			tmp[i+j-1][1]=(tmp[i+j-1][1]+1ll*dp[x][i][1]*dp[y][j][0]+1ll*dp[x][i][0]*dp[y][j][1])%MOD;
		} siz[x]+=siz[y];
		for(int i=1;i<=siz[x];i++) dp[x][i][0]=tmp[i][0],dp[x][i][1]=tmp[i][1];
	}
//	for(int i=1;i<=siz[x];i++) printf("%d %d %d %d
",x,i,dp[x][i][0],dp[x][i][1]);
}
int main(){
	scanf("%d",&n);
	for(int i=1,u,v;i<n;i++) scanf("%d%d",&u,&v),adde(u,v),adde(v,u);
	dfs(1,0);init_fac(n);
	for(int i=1;i<=n;i++) f[n-i]=1ll*dp[1][i][1]*qpow(n,i-2)%MOD;
	for(int i=0;i<n;i++) for(int j=i;j<n;j++){
		if((j-i)&1) g[i]=(g[i]-1ll*f[j]*binom(j,i)%MOD+MOD)%MOD;
		else g[i]=(g[i]+1ll*f[j]*binom(j,i))%MOD;
	}
	for(int i=0;i<n;i++) printf("%d%c",g[i],(i==n-1)?'
':' ');
	return 0;
}

Algorithm 2.

看到“生成树个数”,当然想到矩阵树定理咯(废话

但是本题一开始的树是没有权值的,因此我们要给这棵树赋上恰当的权值。注意到题目相当于从这 (n) 个点的完全图中找出一些生成树,对于生成树中的每条边,如果它也在原树中,那么会对重复边数产生 (1) 的贡献,否则对重复边数产生 (0) 的贡献。因此考虑建一张由这 (n) 个点组成的完全图,对于所有在原树中的边赋上权值 (x),其余的边赋上权值 (1),那么显然所有生成树权值乘积之和是一个多项式 (P),而 (ans_k) 就是 ([x^k]P),这个很好理解。

不过如果我们暴力使用多项式进行计算,那复杂度将会高达 (n^5)不知道能不能过得去,因此考虑使用解多项式题的常用技巧——插值。根据 (n+1) 个点唯一确定一个 (n) 次多项式这个性质,我们考虑令 (x=1,2,cdots,n) 分别跑一遍矩阵树定理,然后高斯消元解出系数即可。当然如果你闲着无聊写个拉格朗日插值或者更快的插值方法那我也不拦着你,瓶颈也不在从 (n) 个点值求出系数处/cy

时间复杂度 (n^4)

代码?抱歉,当时太菜了不会 Matrix-Tree 定理/wq,所以代码就咕着了(((

原文地址:https://www.cnblogs.com/ET2006/p/codeforces-917D.html