Prüfer 序列 学习笔记
引入
Prüfer 序列可以用于求解序列与树的双射,常用于组合计数问题。
定义
Prüfer 序列指的是每次选取一个编号最小的叶子,删除它,然后在序列中记录它所链接的点,重复以上步骤直到只剩下两个节点。
过程
对树建立 Prüfer 序列
显然可以用堆实现一个朴素的 \(\mathcal{O}(n \log n)\) 算法。
但是,效率太低了,其实还存在一个线性的算法。
线性建立 Prüfer 序列
可以发现,每删除一个叶子节点,叶子节点的数量最大增加一个,即叶子节点的数量是非严格单调递减的。
那么可以维护一个指针 \(p\),指向当前编号最小的节点,执行以下操作:
- 删除当前节点 \(p\),然后检查是否出现新的叶子节点。
- 如果出现新的叶子节点 \(x\),如果 \(x < p\) 立刻将 \(x\) 删除,并执行 \(1\) 步骤,否则跳过不管。
- 重复以上操作,知道只剩下两个点。
正确性显然,因为每个点如果小于 \(p\),那么会立刻删除,否则之后肯定会访问到它。
复杂度分析,可以发现每条边最多删除一次,因此时间复杂度显然\(\mathcal{O}(n)\)。
性质
- 根据上述过程,显然 \(n\) 号节点永远不可能删除。
- 每个点出现次数是其度数减一。
通过 Prüfer 序列建立树
根据 Prüfer 序列的性质可以知道每个点的度数,然后可以知道当前编号最小的节点,它必然和现在的序列的第一个数相连,然后同时删除这两个点,最后重复以上步骤就可以得到树,最后再连上剩下的两个节点即可。
这个方法使用堆显然可以 \(\mathcal{O}(n \log n)\) 解决。
但是同样的它还存在一个线性做法
线性建树
同样的维护一个指针 \(p\)。
然后利用建立 Prüfer 序列的过程的思路,就可以实现 \(\mathcal{O}(n)\) 建树。
实现
以下是【模板】Prufer的代码。
#include <cstdio>
#include <vector>
#include <algorithm>
using u32 = unsigned int ;
using i64 = long long ;
using u64 = unsigned long long ;
const int N = 5e6 + 5;
// -------------------- 快读
bool isdigit(int __C){return '0'<=__C&&__C<='9';}
char *p1,*p2,buf[1<<20];
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
template<typename _Tp>
void read(_Tp & x) {
x = 0;
int fh = 1;
char ch = getchar();
while (!isdigit(ch)) {if (ch == '-') fh = -1; ch = getchar();}
while (isdigit(ch)) {x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
x *= fh;
}
template<typename _Tp, typename... _Tps>
void read(_Tp &x, _Tps &...args){
read(x), read(args...);
}
// ---------------------- end
int n, m;
int f[N], p[N];
int deg[N];
std::vector<int> GetPrufer(){
int p = -1;
for (int i = 1; i < n; i++) {
if (deg[i] == 1) {
p = i;
break;
}
}
std::vector<int> code(n - 2);
int leaf = p;
for (int i = 1; i <= n - 2; i++) {
int nxt = f[leaf];
code[i - 1] = nxt;
if (--deg[nxt] == 1 && nxt < p) {
leaf = nxt;
} else {
++p;
while (deg[p] != 1) {
++p;
}
leaf = p;
}
}
return code;
}
std::vector<int> build(){
int ptr = -1;
for (int i = 1; i <= n; i++) {
if (deg[i] == 1) {
ptr = i;
break;
}
}
std::vector<int> tree(n);
int leaf = ptr;
for (int i = 1; i <= n - 2; i++) {
int v = p[i];
tree[leaf] = v;
if (--deg[v] == 1 && v < ptr) {
leaf = v;
} else {
++ptr;
while (deg[ptr] != 1) {
++ptr;
}
leaf = ptr;
}
}
tree[leaf] = n;
return tree;
}
i64 ans;
int main(){
read(n, m);
if (m == 1) {
for (int i = 1; i < n; i++) {
read(f[i]);
deg[i]++, deg[f[i]]++;
}
auto code = GetPrufer();
for (int i = 0; i < n - 2; i++) {
ans ^= (1ll * (i + 1) * code[i]);
}
} else {
for (int i = 1; i <= n; i++) {
deg[i] = 1;
}
for (int i = 1; i <= n - 2; i++) {
read(p[i]);
deg[p[i]]++;
}
auto tree = build();
for (int i = 1; i < n; i++) {
ans ^= (1ll * i * tree[i]);
}
}
printf("%lld", ans);
return 0;
}
凯莱定理
定理
对于一个完全图 \(K_n\) 它的生成树个数为 \(n ^ {n - 2}\) 。
证明
可以通过构造 Prüfer 序列证明,因为是完全图,所以对于任意一个长度为 \(n - 2\) 的序列都能构成一个生成树,因此得证。
拓展
图联通方案
问题
给定一个 \(n\) 个点 \(m\) 条边的 \(k\) 个连通块,要求添加 \(k - 1\) 条边,使得图联通,求方案数。
记号说明
记 \(s_i\) 表示第 \(i\) 个连通块大小, \(d_i\) 为连接后第 \(i\) 个联通块作为树上一个顶点的的度数,因此有 \(\sum\limits_{i = 1}^k d_i = 2k -2\)。
结论
\(\text{方案数} = n ^{k - 2} \prod\limits_{i = 1}^k s_i\)
证明
先对给定的 \(d_i\) 构造 Prüfer 序列,它的方案数有:
然后对于每个联通块内部有 \(s_i^{d_i}\) 种方案(考虑没条边连向哪个点),还要枚举每个联通块的度数,因此总的方案有:
这个式子貌似化简不了了,但是还有一个多元二项式定理可以化简它。
多元二项式定理
因此可以设 \(e_i = d_i - 1\),那么有 \(\sum e_i =k - 2\)。
因此原式可以化为:
可以化简为:
也就是: