浅谈 Prufer(Prüfer) 序列
Bc2_Ch1ckenPr1nce
·
·
算法·理论
Part 0:前置知识
Part 1:什么是 Prufer 序列
从数学角度上来说,Prufer 序列是无根有标号树形结构的双射。即,由一个 Prufer 序列可以得到唯一的树,由一棵树可以得到唯一的 Prufer 序列。
它对树上计数问题和构造问题起到了很大的作用。
Prufer 序列主要解决这三类问题:
- 计算满足特定度数限制的有标号树的数量;
- 根据给定的度数序列构造对应的有标号树;
- 实现树与 Prufer 序列之间的双向转换。
Part 2:Prufer 序列的构造和性质
2.1:如何构造 Prufer 序列
给定一棵树。每一次,我们都在树上找到编号最小的叶子节点,并记录下与它唯一相连的那个父节点的编号,然后将这个叶子节点及其相连的边从树上删除,更新度数。重复这个过程,直到树中只剩下最后 2 个节点为止。过程中记录的父节点编号序列就是 Prufer 序列。
例如,这是一棵 7 个结点的树的 Prufer 序列构建过程:
最终的 Prufer 序列为 [2, 2, 3, 3, 2]。
唯一性证明:
由于 Prufer 序列每一步选择编号最小的叶子节点,而不存在多个最小的叶子节点(编号不可能重复),所以此选择具有唯一性。那么,Prufer 序列是有唯一性的。
2.2:Prufer 序列的性质
性质 1:
在剩余的两个节点中,一定有编号为 n 的节点。
证明:
我们采用反证法进行推导,假设最后留存的两个节点均不包含编号为 n 的节点,那么节点 n 必然在之前的步骤中被当作叶子删除。
节点 n 是所有顶点里编号最大的点,只要图中还存在任意一个编号小于 n 的节点,n 就不可能成为全局编号最小的叶子,自然无法被选中并删除。
上述推论与最初假设相互矛盾,因此假设不成立。即,在剩余的两个节点中,一定有编号为 n 的节点。
性质 2:
设节点编号 i 在 Prufer 序列中出现的次数为 \text{cnt}_i,度数为 \text{deg}_i,则:
\text{cnt}_i + 1 = \text{deg}_i
即出现次数加上 1 为节点 i 的度数。
证明:
对任意标号节点 i,树中该点初始度数为 \text{deg}_i,每一次它作为相邻点被记录进 Prufer 序列,都对应有一个邻接的叶子节点被删除,这会让节点 i 的度数恰好减 1,因此序列中每出现一次 i,其度数就减少 1。
当构造流程终止时,树上仅剩两个节点,此时节点 i 若仍保留在树中,它的度数至少为 1;若已被删除,则它最后一次被记录时度数恰好降为 1,随后就会作为最小叶子被移除。无论哪种情况,节点 i 最终剩余的度数都恒为 1,也就意味着初始度数减去出现次数等于 1,即 \text{deg}_i - \text{cnt}_i = 1。
最后,移项得 \text{cnt}_i + 1 = \text{deg}_i。
Part 3:Prufer 序列和树的互转
3.1:树转 Prufer 序列
算法步骤见 2.1。
::::success[Code]
void ToPrufer() {
fill(deg + 1, deg + n + 1, 1); // 初始化,deg[i] = 1。
for (int i = 1; i < n; ++ i) {
++ deg[fa[i]];
}
int cur; // 当前枚举到的最小编号节点
for (int i = 1; i <= n; ++ i) {
if (deg[i] == 1) {
cur = i;
break;
}
}
int leaf = cur; // 编号最小的叶子节点
for (int i = 1, f; i <= n - 2; ++ i) {
f = fa[leaf], prufer[i] = f;
-- deg[f];
if (deg[f] == 1 && f < cur) {
leaf = f;
continue;
}
++ cur;
for (; deg[cur] != 1; ++ cur);
leaf = cur;
}
}
::::
3.2:Prufer 序列转树
如上文所述,在一个 Prufer 序列中,某个节点编号出现的次数加 1 等于它的度数。我们不停的找到编号最小的叶子结点,然后与 Prufer 序列中的结点依次连边,还原父子关系。注意,虽然我们描述了父子关系,但本质上这是一棵无根树。Prufer 序列只是对树的拓扑结构的映射。
算法步骤:
给定一个长度为 n - 2 的 Prufer 序列 p,还原出对应的 n 个节点的有标号树。
- 初始化一个数组 \text{deg}(即度数),设所有初始值为 1。
- 遍历 Prufer 序列,对于序列 p 中的每个元素 x ,令 \text{deg}_i \to \text{deg}_i + 1,这样就得到了每个点的度数。
- 找到编号最小的度数为 1 的结点,其父节点就是 p_1,连边,并将 \text{deg}_{p_i} \to \text{deg}_{p_i} - 1。
- 重复步骤 3,直到处理完所有 n - 2 个 p 中的元素,最后剩下 2 个结点连边即可。由于是无根有标号树,所以最后谁是根节点不影响。
::::success[Code]
void ToTree() {
fill(deg + 1, deg + n + 1, 1); // 初始化,deg[i] = 1。
for (int i = 1; i <= n - 2; ++ i) {
++ deg[prufer[i]];
}
int cur;
for (int i = 1; i <= n; ++ i) {
if (deg[i] == 1) {
cur = i;
break;
}
}
int leaf = cur;
for (int i = 1, f; i <= n - 2; ++ i) {
fa[leaf] = prufer[i], f = fa[leaf];
-- deg[f];
if (deg[f] == 1 && f < cur) {
leaf = f;
continue;
}
++ cur;
for (; deg[cur] != 1; ++ cur);
leaf = cur;
}
fa[n] = leaf;
}
::::
Part 4:Cayley 公式及其推广
Cayley 公式是 Prufer 序列的一个核心应用。
公式 1:
**证明:**
根据 Prufer 序列的构成,$n$ 个节点的有标号树与长度为 $n - 2$ 的 Prufer 序列一一对应。
而长度为 $n - 2$ 的序列,每个位置可以取 $1$ 至 $n$ 中的任意一个数,共有 $n^{n - 2}$ 种可能。因此,$n$ 个节点的有标号树的数量为 $n^{n - 2}$。
每棵无根树可以选任意一个节点当根,共 $n$ 种选择。所以,有根树数量为 $n^{n - 2} \times n = n^{n - 1}$。
**公式 2:**
对于 $n$ 个节点的完全图,其生成树的方案数为 $n^{n - 2}$。
**证明:**
任何一个生成树都可以由长度为 $n - 2$ 的值域为 $[1, n]$ 的 Prufer 序列映射得到。
由乘法原理,得到方案数为 $n^{n - 2}$。
**公式 3:**
对于 $n$ 个节点的树,点 $i$ 的度数为 $\text{deg}_i$,则满足条件的树的数量为:
$$\frac{(n - 2)!}{\prod_{i = 1}^n (\text{deg}_i - 1)!}$$
**证明:**
根据 Prufer 序列的性质 2,可以得到 $\text{cnt}_i = \text{deg}_i - 1$,即出现次数为点的度数减去 $1$。
所以,问题转化为:在长度为 $n - 2$ 的序列中,需要出现 $\text{deg}_i - 1$ 个节点 $i$,这样的序列有多少个。
这是多重集的排列数问题,详见作者写的 [浅谈排列组合](https://www.luogu.com.cn/article/1kutt8hl)。
套公式即可得到答案为:
$$\frac{(n - 2)!}{(\text{deg}_1 - 1)!(\text{deg}_2 - 1)! \cdots (\text{deg}_n - 1)!}$$
得证。
## Part 5:例题
### [P6086 【模板】Prüfer(Prufer) 序列](https://www.luogu.com.cn/problem/P6086)
模板题。
::::success[Code]
```cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
int n, m;
ll fa[kMaxN], prufer[kMaxN], deg[kMaxN], ans;
void ToPrufer() {
fill(deg + 1, deg + n + 1, 1); // 初始化,deg[i] = 1。
for (int i = 1; i < n; ++ i) {
++ deg[fa[i]];
}
int cur; // 当前枚举到的最小编号节点
for (int i = 1; i <= n; ++ i) {
if (deg[i] == 1) {
cur = i;
break;
}
}
int leaf = cur; // 编号最小的叶子节点
for (int i = 1, f; i <= n - 2; ++ i) {
f = fa[leaf], prufer[i] = f;
-- deg[f];
if (deg[f] == 1 && f < cur) {
leaf = f;
continue;
}
++ cur;
for (; deg[cur] != 1; ++ cur);
leaf = cur;
}
}
void ToTree() {
fill(deg + 1, deg + n + 1, 1); // 初始化,deg[i] = 1。
for (int i = 1; i <= n - 2; ++ i) {
++ deg[prufer[i]];
}
int cur;
for (int i = 1; i <= n; ++ i) {
if (deg[i] == 1) {
cur = i;
break;
}
}
int leaf = cur;
for (int i = 1, f; i <= n - 2; ++ i) {
f = fa[leaf] = prufer[i];
-- deg[leaf], -- deg[f];
if (deg[f] == 1 && f < cur) {
leaf = f;
continue;
}
++ cur;
for (; deg[cur] != 1; ++ cur);
leaf = cur;
}
fa[n] = leaf;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
if (m == 1) {
for (int i = 1; i < n; ++ i) {
cin >> fa[i];
}
ToPrufer();
for (ll i = 1; i <= n - 2; ++ i) {
ans ^= (i * prufer[i]);
}
cout << ans << '\n';
} else {
for (int i = 1; i <= n - 2; ++ i) {
cin >> prufer[i];
}
ToTree();
for (ll i = 1; i < n; ++ i) {
ans ^= (i * fa[i]);
}
cout << ans << '\n';
}
return 0;
}
```
::::
### [P2290 [HNOI2004] 树的计数](https://www.luogu.com.cn/problem/P2290)
Cayley 公式,满足条件的树的数量为:
$$\frac{(n - 2)!}{\prod_{i = 1}^n (d_i - 1)!}$$
但本题的 $n$ 可以达到 $150$,暴力计算 $(n - 2)!$ 的阶乘肯定会炸掉。
第一种写法肯定是高精度。
第二种写法比较好想。从数据范围可以得到,本题答案不超过 $10^{17}$。这可以想到什么?当然是取模了!那么,我们便可以设一个质数模数 $p = 10^{17} + 3$,然后每一次计算都对 $p$ 取模即可。
第三种写法较为小众,运用了勒让德定理。
对于质数 $p$,在 $n!$ 中包含的 $p$ 的指数:
$$\sum_{i = 1}^{\infty} \frac{n}{p^i}$$
对分子分母进行拆解即可。
因为第一种写法太长,第三种写法太麻烦,所以作者运用第二种写法。
请记得判无解(度数减 $1$ 之和不等于 $n - 2$)和 $n \le 2$ 的情况。
需要开 `__int128`。
::::success[Code]
```cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
const __int128 kMod = (ll)1e17 + 3;
ll n, d[kMaxN], sum;
__int128 f[kMaxN], ans;
__int128 Qpow(__int128 a, __int128 b) {
__int128 ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % kMod;
}
a = a * a % kMod;
}
return ans;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n, f[0] = 1;
for (int i = 1; i <= n; ++ i) {
cin >> d[i], sum += d[i] - 1;
}
if (n == 1) {
return cout << !d[1] << '\n', 0;
} else if (n == 2) {
return cout << (d[1] == 1 && d[2] == 1) << '\n', 0;
}
if (sum != n - 2) {
return cout << "0\n", 0;
}
for (int i = 1; i <= 150; ++ i) {
f[i] = f[i - 1] * i % kMod;
}
ans = f[n - 2];
for (int i = 1; i <= n; ++ i) {
if (d[i] != 1) {
ans = ans * Qpow(f[d[i] - 1], kMod - 2) % kMod;
}
}
cout << (ll)ans << '\n';
return 0;
}
```
::::
### [CF156D Clues](https://www.luogu.com.cn/problem/CF156D)
这是 Cayley 公式的一种扩展形式。
**题目翻译(中译中):**
给定一个有 $n$ 个点的图,它被分成了 $k$ 个连通块,每个连通块里有 $s_1, s_2, \dots, s_k$ 个点,问用最少的边把整个图连成一棵树(也就是把这 $k$ 个连通块连起来变成一棵“块树”),总共有多少种连法?
**Solution:**
我们先把每个连通块看成一个整体,也就是一个“超级节点”。要把 $k$ 个超级节点连成一棵树,一共需要 $k - 1$ 条边。
而树的边数等于所有节点度数和的一半,所以这棵“块树”的总度数和是 $2(k - 1)$。
我们记第 $i$ 个连通块在这棵树里的度数是 $d_i$,那么所有连通块的度数加起来就是 $2k - 2$。
普通的 Prufer 序列可以用来数普通树的数量,这里我们用它来数“块树”的结构数。
对于一组度数 $d_1, d_2, \dots, d_k$,对应的 Prufer 序列构造方法数,就等于把 $k - 2$ 个位置分配给各个连通块,让第 $i$ 个连通块出现 $d_i - 1$ 次的排列数。
Prufer 序列只决定了块与块之间的连接结构,但是具体到两个连通块之间的边,要从哪个点连到哪个点呢?
+ 第 $i$ 个连通块的度数是 $d_i$,说明它要连出 $d_i$ 条边,每条边的起点都可以选这个连通块里的任意一个点,所以有 $s_i^{d_i}$ 种选法。
+ 把所有连通块的选法乘起来,就是给定度数序列时,完整的连边方案数。
现在我们要把所有可能的度数序列 $d_1, d_2, \dots, d_k$ 对应的方案数全部加起来。这个求和看起来很复杂,但可以用“多项式定理”来简化。
我们做个变量替换,令 $e_i = d_i - 1$,这样度数和的条件就变成了 $e_1 + e_2 + \dots + e_k = k-2$。
把 $d_i = e_i + 1$ 代入后,求和式刚好可以看成是多项式展开的形式:
$$(s_1 + s_2 + \dots + s_k)^{k - 2} \times s_1 s_2 \cdots s_k$$
而 $s_1 + s_2 + \dots + s_k = n$(总点数),所以最后就化简成了:
$$n^{k - 2} \times \prod_{i = 1}^k s_i$$
代码不给了。