【学习笔记】Prufer 序列
其实一直不会怎么将树和 Prufer 序列互相转换,但是刚刚做题发现要用到,所以去看了眼。
前面的内容复制的之前写的内容。
定义
Prufer 序列是一种将无根树映射到一个序列上,且每种序列都唯一对应一种无根树。
具体构造如下:
- 找出所有叶子节点中编号最小的一个。
- 删除这个叶子节点,并且将这个叶子节点的父节点(所连向的节点)加入数列。
- 持续步骤 1、2,直到节点个数为 2。
也就是说,\(n\) 个节点的 Prufer 序列的长度为 \(n - 2\)。
为什么这样是一一对应的呢?因为这样相当与每一次将一个当前所能加入的最小的节点连接到某个节点下,这 \(n - 2\) 个数字可以看作是 \(n - 2\) 次连边,最一开始有两个节点,他们首先连边,这样就是 \(n - 1\) 条边。
性质
- 某个点的度数等于它在 Prufer 序列中出现的次数 + 1。
每一个数相当于是有一个点向这个数连了一次边,而这个点最开始已经向一个节点连过了边,所以是次数 + 1。 - \(n\) 个点的无根树数量为 \(n^{n-2}\)。
无根树与 Prufer 序列为双射的关系,而 Prufer 序列的长度为 \(n - 2\),每个数都有 \(n\) 种取值,所以是 \(n^{n-2}\) 种。 - 最后剩下的两个点中,一定有一个为 \(n\)。
由于我们每次删除的是编号最小的节点,那么 \(n\) 一定不会被删除。同时这告诉我们,如果以 \(n\) 为树根,那么叶子 \(i\) 连向的节点就是它的父亲节点。
构建 Prufer 序列
显然直接模拟上述的过程就是 \(O(n \log n)\) 的。
我们可以发现,每次删去一个叶子后,要不然不会增加叶子,要不然只会增加一个叶子。那么其实每次删除一个叶子之后如果新增加了一个叶子,我们只需要看新增加的这个叶子是不是编号最小的叶子即可。而除此之外,删除的叶子的编号一定是单调的。这样,我们可以维护一个单调指针,即可做到线性构造 Prufer 序列。
vector<int> e[MAXN];
int n, fa[MAXN], deg[MAXN], pru[MAXN];
void dfs(int u, int pre) {
fa[u] = pre, deg[u] = e[u].size();
for (int v : e[u]) if (v != pre) {
dfs(v, u);
}
}
void build() {
int ptr = 1;
dfs(n, 0);
while (deg[ptr] != 1) ++ptr;
int leaf = ptr;
for (int i = 1; i <= n - 2; i++) {
pru[i] = fa[leaf];
if (--deg[fa[leaf]] == 1 && fa[leaf] < ptr) {
leaf = ptr;
} else {
ptr++;
while (deg[ptr] != 1) ++ptr;
leaf = ptr;
}
}
}
由 Prufer 序列构造树
这个相对于由树构建 Prufer 序列更有用一些。
我们只需要将上面的过程反过来其实就可以了。首先我们可以先确定每个点的度数,然后同样每次删叶子。由于我们知道父亲节点的编号,于是暴力维护就是 \(O(n \log n)\) 的。线性做法与上一个做法完全一致,同样也是每次考虑父亲节点与当前节点的关系即可。
vector<int> e[MAXN];
int n, fa[MAXN], deg[MAXN], pru[MAXN];
void build() {
int ptr = 1;
for (int i = 1; i <= n; i++)
deg[i] = 1;
for (int i = 1; i <= n - 2; i++)
deg[pru[i]]++;
while (deg[ptr] != 1) ++ptr;
int leaf = ptr;
for (int i = 1; i <= n - 2; i++) {
fa[leaf] = pru[i];
if (--deg[fa[leaf]] == 1 && fa[leaf] < ptr) {
leaf = ptr;
} else {
ptr++;
while (deg[ptr] != 1) ++ptr;
leaf = ptr;
}
}
fa[leaf] = n;
}