计树问题小结
标题并没有打错字
前言
某个当时对生成树一窍不通的蒟蒻在\(WC2019T1\)看到了“\(n\)个点的无根树一共有\(n^{n-2}\)种”时感到十分诡异,于是恶补了相关知识但是并没有总结,正好和最近的无标号树的计数问题合在一起
\(Prufer\)序列
Prufer数列是无根树的一种数列。在组合数学中,Prufer数列由有一个对于顶点标过号的树转化来的数列,点数为n的树转化来的Prufer数列长度为n-2。它可以通过简单的迭代方法计算出来。
——百度百科
下面通过给出构造方法来得到这样一个结论:无根树和\(Prufer\)序列是一一对应的关系
无根树转\(Prufer\)序列
1)找到编号最小的叶子结点(度数为1的节点)
2)删除该节点并在\(Prufer\)序列中添加与其有边相连的点(也就是它的父亲)
3)回到操作1直到树中只剩下\(2\)个节点
显然树中一定最后会剩下\(2\)个节点(其实这就是一个类似\(toposort\)的过程),这样就说明了一棵无根树仅对应着一个\(Prufer\)序列
\(Prufer\)序列转无根树
1)构建一个含有\(n\)个点的编号的集合\(G\)
2)取出\(Prufer\)序列的第一个数\(u\)和\(G\)中未在\(Prufer\)序列出现的编号最小的点\(v\)(没有在\(Prufer\)序列出现说明它的度数为\(1\))
3)在\(u\)和\(v\)之间连一条边\((u,v)\),并删去\(u\)和\(v\)
4)重复上述步骤直到\(G\)中只剩两个点,将其连一条边
不难得到这其实是给每一个点找它在树上的\(fa\)的过程(虽然是一棵无根树),于是\(Prufer\)序列也仅对应这一棵无根树
以上我们就得到了这两者之间的一一对应关系,利用这一条性质可以解决一类计数问题
性质
1、\(n\)个点的无根树一共有\(n^{n-2}\)种,有根树有\(n^{n-1}\)种
将每一棵无根树都映射到其对应的\(Prufer\)序列上,由于\(Prufer\)序列的每一位都有\(n\)种取值,于是就一共有\(n^{n-2}\)种;有根树的话再乘上根的种数\(n\)即可
2、设编号\(u\)在\(Prufer\)序列出现过\(d_u\)次,那么编号为\(u\)的节点在树上的度数为\(d_u+1\)
依然把无根树当做有根树来看待,这样的话每个点\(u\)都有一个父亲\(fa_u\),它必须在\(u\)删去之后才有可能被删去,类似的,\(u\)的所有儿子会在\(u\)之前被删去,每一次删去\(u\)的儿子时都会将\(u\)放入\(Prufer\)序列中,所以\(u\)一共有\(d_u\)个儿子,再算上它的父亲就一共有\(d_u+1\)个点与之相连
3、记点\(1,2,\cdots,n\)的度数分别为\(d_1,d-2,\cdots d_n\),那么无根树一共有\(\frac{{n-2}!}{\prod_{i=1}^n(d_i-1)}\)种
由性质\(2\)知,点\(u\)在\(Prufer\)序列中的出现次数为\(d_u-1\),于是问题变成了重复排列的计算
例题:HNOI2004 树的计数:模板题
HNOI2008 明明的烦恼:需要推式子
无标号有根树的计数
看起来很高级,说个简单的例子:烷基计数
这个题有一个弱化版本和一个强化版本,强化版本需要用到生成函数和\(Burnside\)引理,在此按下不表
弱化版本的话直接记\(f_i\)为有\(i\)个顶点的树有多少种,枚举三个儿子的大小进行转移
接下来假设问题是这样的:求\(n\)个点,每个点的度数限制为\(m\)的无根树的方案数
先简单提一下一个引理:\(n\)个元素的\(m\)元素可重集合(集合中的元素可以重复)的种数为\(\dbinom{n+m-1}{n-1}=\dbinom{n+m-1}{m}\)
证明的话可以考虑隔板法,我们可以看成是\(m\)个物品中间放\(n-1\)个隔板分成\(n\)组,每组对应着原来的\(n\)个元素的一个,物品对应着选出来的元素,每一组中的元素表示这几个物品所代表的原来的集合中的元素是这一组所代表的原来的元素,直接做组合即可(注意这里物品是无标号的所以是组合)
记\(f_{i,j}\)为\(i\)个点,根节点度数为\(j\)时的方案数,显然\(Ans=\sum_{i=0}^mf_{n,i}\)
为了方便转移,再设\(a_i=\sum_{j=0}^{m-1}f_{i,j}\),表示了一棵\(i\)个点的子树的方案数
我们枚举度数和每个子树的大小,记当前有\(k_s\)个大小为\(s\)的子树,于是就有下面的转移
\[ f_{i,j}=\sum_{k_1+2k_2+\cdots+jk_j=i}\prod_{s=1}^j\dbinom{a_s+k_s-1}{k_s} \]
后面那个组合数就是说有\(k_s\)个大小为\(s\)的子树的方案数,这就是上面的可重元素的集合,注意每个子树的根的度数要先减去与\(i\)相连的那个\(1\)
这样直接\(dp\)显然是不行的,我们考虑枚举转移时出现的最大子树\(mx\),然后倒序枚举\(i\),顺序枚举\(j\),以此确定\(mx\)的出现次数\(k\),具体的有
\[ f_{i,j}=\sum_{mx=1}^{n-1}\sum_{k=1}^{min(j,\lfloor\frac{i}{mx}\rfloor)}f_{i-mx*k,j-k}\dbinom{a_{mx}+k-1}{k} \]
倒序枚举\(i\)是为了在调用更小的下标是还未计算\(mx\)对其的贡献
复杂度似乎是\(O(n^2mlogm)\),但是在烷基计数时\(m=4\),于是时间复杂度确定为\(O(n^2)\),可以通过
#include<iostream>
#include<string.h>
#include<string>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<vector>
#include<queue>
#include<map>
#include<set>
using namespace std;
#define lowbit(x) (x)&(-x)
#define sqr(x) (x)*(x)
#define fir first
#define sec second
#define rep(i,a,b) for (register int i=a;i<=b;i++)
#define per(i,a,b) for (register int i=a;i>=b;i--)
#define maxd 1000000007
#define eps 1e-6
typedef long long ll;
const int N=5000;
const double pi=acos(-1.0);
int n;
ll fac[5050],invfac[5050],f[5050][5];
int read()
{
int x=0,f=1;char ch=getchar();
while ((ch<'0') || (ch>'9')) {if (ch=='-') f=-1;ch=getchar();}
while ((ch>='0') && (ch<='9')) {x=x*10+(ch-'0');ch=getchar();}
return x*f;
}
ll qpow(ll x,int y)
{
ll ans=1;
while (y)
{
if (y&1) ans=ans*x%maxd;
x=x*x%maxd;
y>>=1;
}
return ans;
}
int main()
{
fac[0]=1;invfac[0]=1;
rep(i,1,N) fac[i]=fac[i-1]*i%maxd;
invfac[N]=qpow(fac[N],maxd-2);
per(i,N-1,1) invfac[i]=invfac[i+1]*(i+1)%maxd;
n=read();f[1][0]=1;
rep(mx,1,n-1)
{
ll a=0;
rep(i,0,3) a=(a+f[mx][i])%maxd;
per(i,n,mx+1)
{
rep(j,1,4)
{
ll tmp=a;int k;
for (k=1;k<=j && mx*k<i;k++)
{
f[i][j]=(f[i][j]+f[i-k*mx][j-k]*tmp%maxd*invfac[k]%maxd)%maxd;
tmp=tmp*(a+k)%maxd;
}
}
}
}
ll ans=0;
rep(i,0,3) ans=(ans+f[n][i])%maxd;
printf("%lld",ans);
return 0;
}
无标号树无根树的计数
对没错它是烷烃计数
那么我们显然会考虑以将一个点看做根,然后将其转化成有根树的计数
注意到树的重心的优越性质:子树的大小不超过\(\lceil\frac{n}{2}\rceil\),于是考虑将重心看做根,这样枚举上界变成了\(\frac{n}{2}\)
但是还要注意一个问题:当\(n\)为偶数的时候,有可能会有两个重心的情况出现(比如一条链),对于这种情况,我们可以考虑将两个大小为\(\frac{n}{2}\)且根节点度数不超过\(m-1\)的两棵树合并起来,于是方案数相比原来还要加上\(\dbinom{a_{\frac{n}{2}+2-1}}{2}\)
上面那道题的代码如下,为了不写高精度于是用了python
dp=[[0 for i in range(5)]for j in range(1010)]
dp[1][0]=1
n=int(input())
for mx in range(1,(n+1)//2):
a=0
for i in range(0,4):
a+=dp[mx][i]
for i in range(n,mx,-1):
for j in range(1,5):
tmp=a
fac=1
for k in range(1,j+1):
if (k*mx<i):
dp[i][j]+=dp[i-k*mx][j-k]*tmp//fac
fac*=(k+1)
tmp*=(a+k)
ans=0
for i in range(0,5) :
ans+=dp[n][i]
if n%2==0:
tmp=0
for i in range(0,4):
tmp+=dp[n//2][i]
ans+=(tmp+1)*tmp//2
print(ans)
参考资料:
prufer序列笔记(自为风月马前卒):https://www.cnblogs.com/zwfymqz/p/8869956.html
无标号树的计数原理(组合计数,背包问题,隔板法,树的重心)(FlashHu):https://www.cnblogs.com/flashhu/p/9457830.html