Prufer 序列
序列可以将一个带标号 nn 个结点的树用 [1,n][1,n] 中的 n−2n−2 个整数表示。也是无根树与数列之间的双射。
对树构造 Prufer 序列 (prufer encoding)
基本操作如下:
- 找到编号最小的叶子结点(入度为11)。
- 将叶子结点的父节点加入Prufer序列。
- 删除这个叶子结点。
按照如上方法执行 n−2n−2 次之后,只剩下两个节点(不难发现其中一个必为 nn)。
可以看看图示。
直接按照构造方式模拟,用堆维护可以达到 O(nlogn)O(nlogn) 的复杂度,但是可以更快。
线性构造
维护一个指针指向我们要删除的结点,不难发现叶结点数量是非严格单调递减的,因为叶结点数量要么不变(删除一个叶结点后产生一个新的叶子结点),要么数量减 11(删除一个叶结点没有产生新的结点)。
所以这样考虑:维护一个指针 pp 开始指向最小的结点,同时维护结点度数以便知晓删除操作后有无新的叶子结点产生。
- 删除 pp 指向的结点,查询是否产生新叶子结点。
- 如果产生新叶子结点,判断新结点 xx 是否小于 pp ,如果 x<px<p,立即删除 xx 并仿照步骤 11 判断是否产生新叶子结点,接着重复 22 直至没有新叶子结点或新叶子结点大于 pp。
- 指针 pp 自增直到指向一个未被删除叶结点为止。
浅浅说一下正确性:
假设 pp 已经指向了当前最小的叶子结点,删除后如果没有产生新结点则不用操作,如产生一个新结点 leafleaf。
如有 leaf>pleaf>p,则 pp 往后扫时一定会扫到 leafleaf,不用做任何事情。
如有 leaf<pleaf<p,则 leafleaf 一定是当前最小的结点,优先删除即可。
按照这种构造方式可以发现每个结点在序列中出现的次数是其度数减 11(叶子结点不出现在序列中)。
发现指针至多遍历每个结点一次,复杂度 O(n)O(n)。
通过 Prufer 序列恢复树 (prufer decoding)
和构造序列类似,我们已经知道所需树中所有结点的度数。 因此我们可以找到所有叶结点,以及在第一步中移除的第一个叶子结点(一定为最小)。 此叶顶点连接到对应于 Prufer 序列第一个数字,然后同时减少这两个结点的度数。
不停重复此操作直至操作完 Prufer 序列的所有数字,我们会剩下两个度数为 11 的点(一个结点必为 nn),按照构造方式模拟用堆维护可以在 O(nlogn)O(nlogn) 内完成
看看图示(红色数字表示当前结点度数不为 00)
线性重构
和线性构造的方法相同,同样发现在减度数的时候可能会产生新的叶结点,用前文的方式维护一个指针 pp ,和新产生的结点比较,优先考虑更小的结点。
复杂度 O(n)O(n)。
代码实现
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
using ll = long long;
int const N = 5e6 + 5;
int n, m;
int a[N], deg[N];
inline void read(int &n) {
int res = 0, f = 1; char c = getchar();
while(!isdigit(c)) {
if(c == '-') f = -1;
c = getchar();
}
while(isdigit(c)) {
res = res * 10 + c - '0';
c = getchar();
}
n = res * f;
}
class purferCode {
int fa[N], pfr[N];
inline void getDegree(int *a, int n) {
for(int i = 1; i <= n; ++ i)
deg[a[i]] ++;
}
public:
inline ll getPrufer(int *fa) {
getDegree(fa, n - 1);
for(int i = 1, p = 1; i < n; ++ i, ++ p) {
while(deg[p]) ++ p;
pfr[i] = fa[p];
while(i < n - 2 and -- deg[pfr[i]] == 0 and pfr[i] < p)
pfr[i + 1] = fa[pfr[i]], ++ i;
}
ll res = 0;
for(int i = 1; i <= n - 2; ++ i)
res xor_eq 1ll * i * pfr[i];
return res;
}
inline ll decodePrufer(int *pfr) {
getDegree(pfr, n - 2);
pfr[n - 1] = n;
for(int i = 1, p = 1; i < n; ++ i, ++ p) {
while(deg[p]) ++ p;
fa[p] = pfr[i];
while(i < n - 1 and -- deg[pfr[i]] == 0 and pfr[i] < p)
fa[pfr[i]] = pfr[i + 1], ++ i;
}
ll res = 0;
for(int i = 1; i < n; ++ i)
res xor_eq 1ll * i * fa[i];
return res;
}
} solve;
int main() {
read(n), read(m);
if(m == 1) {
for_each(a + 1, a + n, read);
printf("%lld\n", solve.getPrufer(a));
} else {
for_each(a + 1, a + n - 1, read);
printf("%lld\n", solve.decodePrufer(a));
}
return 0;
}
通过上述过程可以发现:Prufer 序列与带标号无根树建立了双射关系。
扩展
Prufer 序列还有其他功能,比如:
证明 Cayley 公式
完全图 GnGn 有 nn−2nn−2 棵生成树(UVA10843 Anne's game)。
不难证明任意一个长度为 n−2n−2 的 Prufer 序列都可以构造出一个各不相同的生成树,值域在 [1,n][1,n] 故总数为 nn−2nn−2。
nn 个结点的有根树计数
对每棵无根树来说,每个点都可能是根,即总数为 nn−1nn−1。
nn 个点的每个度数分别为 didi 的无根树计数(P2290 [HNOI2004]树的计数)
总排列为 (n−2)!(n−2)!,即 An−2n−2An−2n−2 。其中数字 ii 出现了 di−1di−1 次,故其重复的有 (di−1)!(di−1)! 种排列,即 Adi−1di−1Adi−1di−1 。应当除去重复的,故总个数为 (n−2)!∏i=1n(di−1)!∏i=1n(di−1)!(n−2)!。