prufer序列
prufer序列,一种把有标号树用唯一的整数序列表示。它可以将一个带标号\(n\)个结点的树用\(n-2\)个整数表示。
建立方法非常简单:每次找到无根树上编号最小的一个叶子,删掉它并记录它的父亲编号,重复\(n-2\)次,直到只剩下两个节点结束。
我们可以以线性的复杂度使一棵树在树和prufer序列之间相互转换。(以下树的形态为其父亲序列,即序列中\(f_i\)为节点\(i\)在\(n\)为根时的父亲)
- 从树到prufer序列
当然我们可以\(O(n^2)\)暴力找每个最小的叶子删掉然后记录父亲。当然仅限于可以,能不能过两码事。
当然我们还可以开个堆保存每个节点的度数,复杂度\(O(n\log n)\)。
但是更优秀的方法是拿指针扫一遍。指针从小到大扫,找到第一个指针扫到的叶子并记录它的父亲,父亲度数\(-1\)。如果这一操作产生了新的比当前节点小的叶子,则继续加入序列中。
void ftp(){
for(int i=1;i<n;i++){
scanf("%lld",&f[i]);
d[f[i]]++;
}
for(int i=1,j=1;i<=n-2;i++,j++){
while(d[j])j++;
p[i]=f[j];
while(i<=n-2&&!--d[p[i]]&&p[i]<j){
p[i+1]=f[p[i]];i++;
}
}
}
这个做法显然正确:当产生新的叶子时:
- 如果叶子比当前节点大,直接不管。
- 反之它一定是最小的叶子,直接更新。
树转成prufer序列就这样。
- 从prufer序列到树
和上一个的逻辑差不多,就是有个要注意的点,最后多放一个根要不然根转不完。
void ptf(){
for(int i=1;i<=n-2;i++){
scanf("%lld",&p[i]);d[p[i]]++;
}
p[n-1]=n;
for(int i=1,j=1;i<n;i++,j++){
while(d[j])++j;
f[j]=p[i];
while(i<n&&!--d[p[i]]&&p[i]<j){
f[p[i]]=p[i+1];i++;
}
}
}
好了上面的东西都是纯扯淡,应该没人考你这个。我瞎编了两个用处:一个是你造随机树的时候可能会用到,一个是敲暴力枚举所有树的时候可能有用。
引用一句话:
显然你不会想不开拿这玩意儿去维护树结构。这玩意儿常用组合计数问题上。
$\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ $——OI WiKi
然后是这玩意真正的应用:计数。
举个例子:凯莱定理:\(n\)个点的完全图有\(n^{n-2}\)棵生成树。
证明:显然\(K_n\)的prufer序列有\(n-2\)个数,每个数都可以从\([1,n]\)中选,总共\(n^{n-2}\)种。
另一个小结论:
对于给定度数为\(d_{1\sim n}\)的一棵无根树共有$\frac{(n-2)!}{\prod_{i=1}^n(d_i-1)!} $种情况。证明很简单,prufer序列上跑重排列就行了。
upd:扩展Cayley定理: \(n\) 个点, \(k\) 个连通块,每个大小为 \(a_i\) ,生成树个数为
\(n\) 个标号节点形成一个有 \(k\) 颗树的森林,使得给定的 \(k\) 个点没有两个点属于同一颗树的方案数为 \(k\times n^{n−k−1}\) 。
模拟赛因为忘了这个保龄。警钟长鸣。