prufer序列小记
\(\text{prufer}\) 序列指的是把一个 \(n\) 个节点的树,转化为一个长度为 \(n-2\) 的值域为 \([1,n]\) 的一个序列。这两个组成一个双射。(一棵树对应唯一的一个 \(\text{purfer}\) 序列,一个 \(\text{prufer}\) 序列对应唯一一棵树)。
prufer序列的构造
\(\text{prufer}\) 序列是这样构造的:
我们每次选一个编号最小的叶子结点,把和他相连的节点(它父亲)放进序列末端,度数减一。直到最后剩下两个节点为止。
这个我们拿堆模拟一下就可以做到 \(O(n\log n)\) 的做法。
\(O(n)\) 做法: 用指针代替堆,具体来说就是指针指向编号最小的叶子节点,每次删掉他后,如果产生了一个新的叶子节点且比当前叶子节点的编号要小,则继续删掉新的叶子结点,否则找到下一个编号最小的叶子结点。
\(\text{prufer}\) 序列转化为树
由 \(\text{prufer}\) 序列,我们可以得到每个点的度数。
我们每次找一个编号最小的叶子节点,让其与当前枚举到的 \(\text{prufer}\) 序列位置上的点连边,同时两个点的度数减一。重复 \(n-2\) 次就会剩下两个点,把这两个点连起来就好了。
\(O(n\log n)\) 的做法就不说了。
\(O(n)\) 做法:还是用指针代替堆,指针指向编号最小的叶子节点,每次把他与当前枚举到的 \(\text{prufer}\) 序列上的点连边,如果新产生了一个新叶子节点且比当前叶子节点的编号要小,则继续把新叶子节点和 \(\text{prufer}\) 序列上的点连边,否则找到下一个编号最小的叶子节点,继续连边。
模板题代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<cmath>
#include<queue>
using namespace std;
#define LL long long
const int N = 5e6+10;
int n,opt,f[N],p[N],du[N];
LL ans;
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
return s * w;
}
void slove1()//树转prufer序列
{
for(int i = 1; i <= n-1; i++) f[i] = read(), du[f[i]]++;
for(int i = 1, j = 1; i <= n-2; i++, j++)
{
while(du[j]) j++;
p[i] = f[j];
while(i <= n-2 && (--du[p[i]] == 0) && p[i] < j) p[i+1] = f[p[i]], i++;
}
for(int i = 1; i <= n-2; i++) ans ^= 1LL * p[i] * i;
printf("%lld\n",ans);
}
void slove2()//prufer序列转树
{
for(int i = 1; i <= n-2; i++) p[i] = read(), du[p[i]]++;
p[n-1] = n;
for(int i = 1, j = 1; i <= n-1; i++, j++)
{
while(du[j]) j++;
f[j] = p[i];
while(i <= n-1 && (--du[p[i]] == 0) && p[i] < j) f[p[i]] = p[i+1], i++;
}
for(int i = 1; i <= n-1; i++) ans ^= 1LL * f[i] * i;
printf("%lld\n",ans);
}
int main()
{
n = read(); opt = read();
if(opt == 1) slove1();
else slove2();
return 0;
}
\(\text{prufer}\) 序列的性质
从上面的构造过程中,不难发现有这样几个性质:
- 每个点在 \(\text{prufer}\) 序列中出现的次数为其度数减1。
- \(n\) 个有标号的点的无根树的个数为 \(n^{n-2}\) 。
- 每个点的度数为 \(d_i\) ,其组成的树的个数为: \(\displaystyle (n-2)!\over \prod_{i=1}^{n} (d_i-1)!\)
- \(n\) 个有编号的点,组成一个有 \(k\) 个联通块的无向连通图,加入 \(k-1\) 条边是这张图变成一棵树的方案数为 \(n^{k-2}\prod_{i} s_i\) (\(s_i\) 表示每个联通块的大小)
证明:
性质1:每个点当且仅当其儿子被删掉的时候会被加入到 \(\text{prufer}\) 序列中,每个节点的儿子数为其度数减1,所以其在 \(\text{prufer}\) 序列中出现的次数为其度数减1。
性质2:无根树的个数等价于 \(\text{prufer}\) 序列的个数,显然为 \(n^{n-2}\) 。
性质3:每个点的度数为 \(d_i\) 等价于其在 \(\text{prufer}\) 序列上出现的次数为 \(d_i-1\), 问题就转化为每个数在一个序列出现了 \(d_i-1\) 次,问你序列的个数。答案显然为:\({n-2\choose d_1-1}\times {n-2-d_1-1\choose d_2-1} ...\) ,化简一下就可以得到: \(\displaystyle (n-2)!\over {\prod_{i-1}^{n}(d_i-1)!}\)
性质4:不会证,详情可以见 oiwiki 上的证明。