Prufer编码

# PruferPrufer 序列

用于对无向无根树与一个数列双射

#\rightarrow PruferPrufer

# 步骤

1.1. 找编号最小的叶节点
2.2. 输出与这个叶节点相连的编号
3.3. 删去这个叶节点

# 模拟

我们创建这样一棵树的 PruferPrufer 编码

图示找编号最小的叶节点输出与该节点相连的编号删去该叶节点
1133
2255
3344
4455
剩两个不输出

那么构造出来的 PruferPrufer 编码就是 {3,5,4,3}\{3,5,4,3\}

# 线性构造

从小到大枚举 jj 表示编号最小的度数为1的点
一旦走到一个度数为 11 的点,这个就是最小的叶节点,输出它的相连节点同时删除它
此时最多又暴露出一个点 xx
1.1. x>jx\gt j不用管,反正会继续向上枚举
2.2. x<jx\lt jjj 本身就是最小的,新来了一个比 jj 更小的,那么新叶节点就是最小的

# 程序

namespace Prufer {
        int f[N]; // 每个节点的父亲节点
        int d[N]; // 度数
        int p[N]; // prufer编码
        inline void Tree2Prufer () {
	        for ( int i = 1; i < n; i ++ )
                        cin >> f[i],
                        d[f[i]] ++;
                for ( int i = 1, j = 1; i <= n - 2; j ++ ) {
                        while ( d[j] ) j ++; // 直到找到一个叶子结点
                        p[ i ++ ] = f[j];    // 这个叶子结点是最小的
                        while ( i <= n - 2 && /*在prufer范围内*/
                                -- d[p[i - 1]] == 0 && /*之前叶子结点删去后的度数*/ 
                                p[i - 1] < j ) /*满足这个点比上一个删去的叶子结点小*/
                                p[ i ++ ] = f[p[i - 1]]; // 新prufer是上一次删去的叶子节点的父亲
                }
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# PruferPrufer\rightarrow

# 步骤 &\And 线性构造

根据已给的编码,我们可以得到每个节点有几个儿子
即除了最后一个点之外,每个点的儿子个数就是它在编码中的出现次数
我们可以模拟“ 树 Prufer\rightarrow Prufer ”的构造方式,只不过我们建树都是在确定一个(也就是上一种构造删边)时建边的
仍然从小到大枚举,当枚举到一个儿子个数为 00 的节点 xx 时便是能确定这个是它在树中第一个删去
同时也是第一个编码的子节点,那就说明</span style="color: red;">第一个编码就是 xx 的父节点,于是建边
假装将这个小的叶子节点删去,那么就需要xx 的子节点数量减 11
在“ 树 Prufer\rightarrow Prufer ”中我们删点后会出现的情况在这里也会出现
所以一样的流程进行判断即可

# 模拟

我们创建这么一个 PruferPrufer 编码的树
{3,5,4,5}\{3,5,4,5\}

首先确定每个节点的子节点数量

节点子节点数量
1100
2200
3311
4411
5522
6611

然后开始按上面说的模拟构造

节点-子节点数量得到叶子结点建树
节点子节点数量
1100
2200
33111-1
4411
5522
6611
11
节点子节点数量
2200
3300
4411
55221-1
6611
22
节点子节点数量
3300
44111-1
5511
6611
33
节点子节点数量
4400
55111-1
6611
44
节点子节点数量
5500
6611
最后 565,6 连边

这样树就建出来了

# 程序

基本上就是上一种构造的反向构造,就不过多赘述了

namespace Prufer {
        int f[N], d[N], p[N];
        inline void Prufer2Tree () {
                for ( int i = 1; i <= n - 2; i ++ )
                        cin >> p[i],
                        d[p[i]] ++;
                p[n - 1] = n;
                for ( int i = 1, j = 1; i < n; i ++, j ++ ) {
                        while ( d[j] ) j ++;
                        f[j] = p[i];
                        while ( i < n - 1 && -- d[p[i]] == 0 && p[i] < j ) f[p[i]] = p[i + 1], i ++;
                }
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 性质

主要是使用下面几个性质实现树上计数


$1.$ $Prufer$ 编码与无根树形成双射
证明: 比较明显了, $Prufer$ 编码就是这么引出的 (也写不出反例了

2.2. 一棵 nn 个节点构成的无向完全图的生成树个数有 nn2n^{n-2}
证明:
对于一个 PruferPrufer 编码,它每个位置能有 nn 个数都可以选择,编码长度为 n2n-2
则总个数有 nn2n^{n-2} 个放置方式

3.3. 度数为 did_i 的节点在 PruferPrufer 编码中会出现 di1d_i-1
证明:
它的 di1d_i-1 个子节点要被删去,每次都会输出一次它,则一共有 di1d_i-1

3推论.3-推论. 对于一个已知度数序列 d[1,n]d_{[1,n]} 它可以形成的无根树数量为 (n2)!i=1m(di1)!\frac{(n-2)!}{\prod\limits_{i=1}m(d_i-1)!}
证明:
长度为 n2n-2 的全排列有 (n2)!(n-2)!
由于每个 did_i 出现 di1d_i-1 次,则每个 did_i 会重复 (di1)!(d_i-1)!
在整个 (n2)!(n-2)! 全排列中去重 (di1)!(d_i-1)! 得到 (n2)!i=1m(di1)!\frac{(n-2)!}{\prod\limits_{i=1}m(d_i-1)!}
别的性质还有待发掘...

# 例题

洛谷P6086 【模板】 PurferPurfer 序列
题目地址
题解地址

洛谷P4981 父子
题目地址
题解地址

洛谷P2290 [HNOI2004]树的计数
题目地址
题解地址

AcWing2418 光之大陆
题目地址
题解地址

Last Updated: 10/14/2023, 7:51:49 PM