[学习笔记] Prüfer 序列
定义
Prüfer 序列是带标号无根树和序列的双射
Prüfer 序列可以用一个长度为 \(n-2\) 的整数序列来表示一棵有 \(n\) 个节点的无根树
其常用在树的计数问题中
构造
树->序列
Prüfer 序列构造的根本在于每次选取编号最小的叶节点,将其删除并加入序列,直到剩下 \(2\) 个节点
方法一
用小根堆维护编号最小的度数为 \(1\) 的点并修改度数
时间复杂度 \(O(n\log n)\)
方法二
发现带个 \(\log\) 太慢了,考虑线性构造
本质是用指针来维护我们将要删除的节点
记 \(parent_u\) 表示节点 \(u\) 的父亲节点
首先,用指针 \(ptr\) 指向编号最小的叶节点
其次,将其父亲节点 \(parent_{ptr}\) 加入序列并删去节点 \(ptr\)
然后,再用一个指针 \(leaf\) 指向,其父亲节点 \(parent_{ptr}\)
若 \(leaf\) 为叶节点且 \(leaf<ptr\),说明现在 \(leaf\) 为编号最小的叶节点
处理 \(leaf\),再令 \(leaf\) 指向 \(parent_{leaf}\) 并重复判断并处理,直到 \(leaf>ptr\)
否则,让 \(ptr\) 指向下一个节点,直到找到某个节点的度数为 \(1\) , 再重复以上步骤
容易发现,每个节点最多只会被处理一次,因此复杂度为 \(O(n)\)
以下是代码参考
vector<vector<int>> adj;
vector<int> parent;
void dfs(int v) {
for (int u : adj[v]) {
if (u != parent[v]) parent[u] = v, dfs(u);
}
}
vector<int> pruefer_code() {
int n = adj.size();
parent.resize(n), parent[n - 1] = -1;
dfs(n - 1);
int ptr = -1;
vector<int> degree(n);
for (int i = 0; i < n; i++) {
degree[i] = adj[i].size();
if (degree[i] == 1 && ptr == -1) ptr = i;
}
vector<int> code(n - 2);
int leaf = ptr;
for (int i = 0; i < n - 2; i++) {
int next = parent[leaf];
code[i] = next;
if (--degree[next] == 1 && next < ptr) {
leaf = next;
} else {
ptr++;
while (degree[ptr] != 1) ptr++;
leaf = ptr;
}
}
return code;
}
序列->树
由 Prüfer 序列可知每个点度数,考虑维护当前编号最小的叶子
我们需要重复进行以下操作,直至点集中只剩下两个点:
1.取出点集中最小的不在 Prüfer 序列中的元素 \(x\)
2.取出 Prüfer 序列最前面的元素 \(y\)
3.将 \(x\) 与 \(y\) 连边
注意:上述的取出相当于删除
最后,点集中会剩下两个点,将它们连边即可
显然我们只会连 \(n-1\) 条边,且绝对不会连成环,且其连成的就是原树
性质
1.编号最大的点一定不会被删除
证明:
正确性显然,因为一棵节点数量大于等于 \(2\) 的树至少有两个叶子
因此即使编号最大的点是叶子节点也不会被删除
2.每个点在 Prüfer 序列中的出现次数为其度数减 \(1\)
证明:
对于除根节点外的所有点,他们都会被儿子加入序列,故它们出现次数为度数减 \(1\)
对于根节点,其最终不会被被删去,即其会成为剩下来的点
故其会被除了剩下来的另外一个点以外的儿子加入序列,其出现次数为度数减 \(1\)

浙公网安备 33010602011771号