[学习笔记] Prüfer 序列

定义

Prüfer 序列是带标号无根树和序列的双射

Prüfer 序列可以用一个长度为 n2 的整数序列来表示一棵有 n 个节点的无根树

其常用在树的计数问题中

构造

树->序列

Prüfer 序列构造的根本在于每次选取编号最小的叶节点,将其删除并加入序列,直到剩下 2 个节点

方法一

用小根堆维护编号最小的度数为 1 的点并修改度数

时间复杂度 O(nlogn)

方法二

发现带个 log 太慢了,考虑线性构造

本质是用指针来维护我们将要删除的节点

parentu 表示节点 u 的父亲节点

首先,用指针 ptr 指向编号最小的叶节点

其次,将其父亲节点 parentptr 加入序列并删去节点 ptr

然后,再用一个指针 leaf 指向,其父亲节点 parentptr

leaf 为叶节点且 leaf<ptr,说明现在 leaf 为编号最小的叶节点

处理 leaf,再令 leaf 指向 parentleaf 并重复判断并处理,直到 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.将 xy 连边

注意:上述的取出相当于删除

最后,点集中会剩下两个点,将它们连边即可

显然我们只会连 n1 条边,且绝对不会连成环,且其连成的就是原树

性质

1.编号最大的点一定不会被删除

证明:

正确性显然,因为一棵节点数量大于等于 2 的树至少有两个叶子

因此即使编号最大的点是叶子节点也不会被删除

2.每个点在 Prüfer 序列中的出现次数为其度数减 1

证明:

对于除根节点外的所有点,他们都会被儿子加入序列,故它们出现次数为度数减 1

对于根节点,其最终不会被被删去,即其会成为剩下来的点

故其会被除了剩下来的另外一个点以外的儿子加入序列,其出现次数为度数减 1

posted @   Leafy_Tree  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示