[学习笔记] 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\)

posted @ 2025-01-19 14:53  Leafy_Tree  阅读(31)  评论(0)    收藏  举报