6494. 【GDOI2020模拟03.08】勘探

题目

求大小为(n)的直径为(L)(指边数)的无标号无根树数量。


思考历程

一开始乱推了一波,后来发现会算重。
于是弃疗。


正解

如果(L)为偶数,所有直径的中点是一样的;如果(L)为奇数,所有直径的中边是一样的。
这个挺好证明的。

把无根树定了根之后就好做很多了。先考虑(L)为奇数,把树从中边切开,形成两棵树,这两棵树都要满足最大深度为恰好为(frac{L-1}{2})
(g_{i,j})为大小为(i),最大深度恰好为(j)的树的个数、
这个东西不好算,就设(f_{i,j})为大小为(i),最大深度至多为(j)的1个数。
转移的时候枚举新增子树大小(k)及其个数(t)
需要注意的是转移的时候循环变量的枚举顺序,(j)放最外层,(k)放次外层。可以这么理解:把(j)不同的每一层看成个生成函数,计算大小为(k)的子树的贡献的时候给整一层计算贡献,不然容易算重。
搞完之后,枚举左右子树大小即可以求出答案(注意大小相等的情况)。

如果(L)为偶数,相当于以中点为根的树中,根有至少两个儿子的子树的深度恰好为(frac{L}{2}-1)(已经减去了根到儿子的边)。同样像上面一样求出(f)(g)
计算答案考虑总体减不合法,总体是(g_{n,frac{L}{2}}),不合法的就是根只有一个儿子的子树深度恰好为(frac{L}{2}-1),枚举其大小(i),贡献即为(g_{i,frac{L}{2}-1}f_{n-i,frac{L}{2}-1})


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 210
#define ll long long
int n,L,mo;
ll inv[N];
ll f[N][N],g[N][N];
void dp(int n,int lim){
	for (int j=0;j<=lim;++j)
		f[1][j]=1;
	for (int j=1;j<=lim;++j)
		for (int k=1;k<n;++k){
			ll tmp=f[k][j-1];
			for (int i=n;i>=2 && i>k;--i){
//				if (i==5)
//					printf("i=5
");
				ll s=tmp;
				for (int t=1;k*t<i;++t,s=s*(tmp+t-1+mo)%mo*inv[t]%mo)
					(f[i][j]+=f[i-k*t][j]*s)%=mo;
			}
		}
	for (int i=1;i<=n;++i){
		g[i][0]=f[i][0];
		for (int j=1;j<=lim && j<i;++j)
			g[i][j]=(f[i][j]-f[i][j-1]+mo)%mo;
	}
//	for (int i=1;i<=n;++i,printf("
"))
//		for (int j=0;j<=lim && j<i;++j)
//			printf("%d ",g[i][j]);
}
int main(){
	freopen("exploit.in","r",stdin);
	freopen("exploit.out","w",stdout);
	scanf("%d%d%d",&n,&L,&mo);
	inv[1]=1;
	for (int i=2;i<=n;++i)
		inv[i]=(mo-mo/i)*inv[mo%i]%mo;
	int lim=L/2;
	dp(n,lim);
	ll ans=0;
	if (L&1){
		for (int i=lim;n-i>=lim && i<=n-i;++i){
			if (i==n-i)
				ans+=g[i][lim]*(g[i][lim]+1)%mo*inv[2]%mo;
			else
				ans+=g[i][lim]*g[n-i][lim]%mo;
		}
		ans%=mo;
	}
	else{
		ans=g[n][lim];
		if (lim>=2)
			for (int i=lim;i<n;++i)
				ans-=g[i][lim-1]*f[n-i][lim-1]%mo;
		ans=(ans%mo+mo)%mo;
	}
	printf("%lld
",ans);
	return 0;
}

总结

见到无根树的时候要想办法给它定个根。
什么直径的中点中边,或者重心之类的。

原文地址:https://www.cnblogs.com/jz-597/p/12459440.html