Prufer 序列
本文翻译自 e-maxx Prufer Code。另外解释一下,原文的结点是从
这篇文章介绍 Prufer 序列 (Prufer code),这是一种将带标号的树用一个唯一的整数序列表示的方法。
使用 Prufer 序列可以证明凯莱定理 (Cayley's formula)。并且我们也会讲解如何计算在一个图中加边使图连通的方案数。
注意:我们不考虑含有
Prufer 序列¶
Prufer 序列可以将一个带标号
显然你不会想不开拿这玩意儿去维护树结构。这玩意儿常用组合计数问题上。
Heinz Prufer 于 1918 年发明这个序列来证明凯莱定理。
对树建立 Prufer 序列¶
Prufer 是这样建立的:每次选择一个编号最小的叶结点并删掉它,然后在序列中记录下它连接到的那个结点。重复
显然使用堆可以做到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // C++ Version
// 代码摘自原文,结点是从 0 标号的
vector<vector<int>> adj;
vector<int> pruefer_code() {
int n = adj.size();
set<int> leafs;
vector<int> degree(n);
vector<bool> killed(n, false);
for (int i = 0; i < n; i++) {
degree[i] = adj[i].size();
if (degree[i] == 1) leafs.insert(i);
}
vector<int> code(n - 2);
for (int i = 0; i < n - 2; i++) {
int leaf = *leafs.begin();
leafs.erase(leafs.begin());
killed[leaf] = true;
int v;
for (int u : adj[leaf])
if (!killed[u]) v = u;
code[i] = v;
if (--degree[v] == 1) leafs.insert(v);
}
return code;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # Python Version
# 结点是从 0 标号的
adj = [[]]
def pruefer_code():
n = len(adj)
leafs = set()
degree = [] * n
killed = [False] * n
for i in range(1, n):
degree[i] = len(adj[i])
if degree[i] == 1:
leafs.intersection(i)
code = [] * (n - 2)
for i in range(1, n - 2):
leaf = leafs[0]
leafs.pop()
killed[leaf] = True
for u in adj[leaf]:
if killed[u] == False:
v = u
code[i] = v
if degree[v] == 1:
degree[v] = degree[v] - 1
leafs.intersection(v)
return code
|
给一个例子吧,这是一棵 7 个结点的树的 Prufer 序列构建过程:
最终的序列就是
当然,也有一个线性的构造算法。
线性构造¶
线性构造的本质就是维护一个指针指向我们将要删除的结点。首先发现,叶结点数是非严格单调递减的。要么删一个,要么删一个得一个。(翻译到这突然就知道该怎么做了,然后对照原文发现没什么问题,于是自己口糊吧)
于是我们考虑这样一个过程:维护一个指针
- 删除
p - 如果产生新的叶结点,假设编号为
x p,x x>p x x 2 >p - 让指针
p
循环上述操作
- 如果
x>p p - 如果
x<p p x p x x
算法复杂度分析,发现每条边最多被访问一次(在删度数的时侯),而指针最多遍历每个结点一次,因此复杂度是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // C++ Version
// 从原文摘的代码,同样以 0 为起点
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;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # Python Version
# 同样以 0 为起点
adj = [[]]
parent = [] * n
def dfs()v:
for u in adj[v]:
if u != parent[v]:
parent[u] = v
dfs(u)
def pruefer_code():
n = len(adj)
parent[n - 1] = -1
dfs(n - 1)
ptr = -1
degree = [] * n
for i in range(0, n):
degree[i] = len(adj[i])
if degree[i] == 1 and ptr == -1:
ptr = i
code = [] * (n - 2)
leaf = ptr
for i in range(0, n - 2):
next = parent[leaf]
code[i] = next
if degree[next] == 1 and next < ptr:
degree[next] = degree[next] - 1
leaf = next
else:
ptr = ptr + 1
while degree[ptr] != 1:
ptr = ptr + 1
leaf = ptr
return code
|
Prufer 序列的性质¶
- 在构造完 Prufer 序列后原树中会剩下两个结点,其中一个一定是编号最大的点
n - 每个结点在序列中出现的次数是其度数减
1
用 Prufer 序列重建树¶
重建树的方法是类似的。根据 Prufer 序列的性质,我们可以得到原树上每个点的度数。然后你也可以得到度数最小的叶结点编号,而这个结点一定与 Prufer 序列的第一个数连接。然后我们同时删掉这两个结点的度数。
讲到这里也许你已经知道该怎么做了。每次我们选择一个度数为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 原文摘代码
vector<pair<int, int>> pruefer_decode(vector<int> const& code) {
int n = code.size() + 2;
vector<int> degree(n, 1);
for (int i : code) degree[i]++;
set<int> leaves;
for (int i = 0; i < n; i++)
if (degree[i] == 1) leaves.insert(i);
vector<pair<int, int>> edges;
for (int v : code) {
int leaf = *leaves.begin();
leaves.erase(leaves.begin());
edges.emplace_back(leaf, v);
if (--degree[v] == 1) leaves.insert(v);
}
edges.emplace_back(*leaves.begin(), n - 1);
return edges;
}
|
线性时间重建树¶
同线性构造 Prufer 序列的方法。在删度数的时侯会产生新的叶结点,于是判断这个叶结点与指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 原文摘代码
vector<pair<int, int>> pruefer_decode(vector<int> const& code) {
int n = code.size() + 2;
vector<int> degree(n, 1);
for (int i : code) degree[i]++;
int ptr = 0;
while (degree[ptr] != 1) ptr++;
int leaf = ptr;
vector<pair<int, int>> edges;
for (int v : code) {
edges.emplace_back(leaf, v);
if (--degree[v] == 1 && v < ptr) {
leaf = v;
} else {
ptr++;
while (degree[ptr] != 1) ptr++;
leaf = ptr;
}
}
edges.emplace_back(leaf, n - 1);
return edges;
}
|
通过这些过程其实可以理解,Prufer 序列与带标号无根树建立了双射关系。
Cayley 公式 (Cayley's formula)¶
完全图
怎么证明?方法很多,但是用 Prufer 序列证是很简单的。任意一个长度为
图连通方案数¶
Prufer 序列可能比你想得还强大。它能创造比凯莱定理更通用的公式。比如以下问题:
一个
个点 n 条边的带标号无向图有 m 个连通块。我们希望添加 k 条边使得整个图连通。求方案数。 k-1
设
对于第
现在我们要枚举
好的这是一个非常不喜闻乐见的式子。但是别慌!我们有多元二项式定理:
那么我们对原式做一下换元,设
化简得到
即
这就是答案啦
习题¶
- UVA #10843 - Anne's game
- Timus #1069 - Prufer Code
- Codeforces - Clues
- Topcoder - TheCitiesAndRoadsDivTwo
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用