Prufer序列
\(Prufer\)
关于 \(Prufer\) 序列
既然提到树,那就必定会提到 \(Prufer\) 序列。
\(Prufer\) 序列是为了证明 \(Cayley\) 公式而被发明出来的,即一个 \(n\) 个点的完全图中共有 \(n^{n-2}\) 个不同的树,然而却拥有更加强大的功能。
\(Prufer\) 序列可以将一棵 \(n\) 个点的树唯一映射到一个长度为 \(n-2\) 的序列上,可以理解为,两棵树不相同,当且仅当它们的 \(Prufer\) 序列不同。
如何构造一棵树的 \(Prufer\) 序列?
每次找到编号最小的一个叶子节点,将与其连接的那个点加入序列,并将其在树上删除,直到树上只有两个节点。
从 \(oi\)-\(wiki\) 上盗一张图,方便理解。
显然 \(Prufer\) 序列会有一下两个性质。
1、树中最后剩下的两个点中,一定会有一个节点是编号最大的 \(n\) 点;
2、每个节点在序列中出现的次数为这个点的度数-1。
思考为什么 \(Prufer\) 序列要这样构造?为什么它可以唯一表示一棵树?
尝试证明:
不难发现,由于 \(Prufer\) 序列的构造方法十分巧妙,也就满足许多特殊的性质。
在构造序列时,每次将与编号最小的叶子节点加入到序列中,而编号最小的叶子节点是很容易根据 \(Prufer\) 序列求出的,因此序列中的每个数表示的不是一个点而是一条边,这样就有了 \(n-2\) 条边,而最后一条边显然是与 \(n\) 号节点相连的。以上,\(Prufer\) 序列就表示出了 \(n-1\) 条边,也就唯一表示出了一棵树。
线性的构造方法
接下来考虑如何构造一棵树的 \(Prufer\) 序列,以及如何通过 \(Prufer\) 序列重构一棵树。
Luogu P6086 【模板】Prufer 序列
存在优秀的线性做法。
令指针 \(p\) 指向最小的叶子节点,将其删除,删掉这个点必然会有一个点度数-1,若出现了一个新的叶子节点,且比 \(p\) 小,那就继续删除,直至新出现的叶子节点比 \(p\) 大或没有新的叶子节点。
让 \(p\) 指向新的最小叶子节点,并重复上述操作。
重构树的方法与构造 \(Prufer\) 序列类似,相信聪明的你一定能想出来。实际上就是我懒的写了。。。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e6+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m;
vector<int> l[N];
int fa[N], p[N], deg[N], tot;
ll ans;
inline void del(int x, int y) { p[++tot]=fa[x]; if(--deg[fa[x]]==1&&fa[x]<y) del(fa[x], y); }
inline void add(int x, int y) { fa[x]=p[++tot]; if(--deg[fa[x]]==0&&fa[x]<y) add(fa[x], y); }
int main(void){
n=read(), m=read(); p[n-1]=n;
if(m==1){
for(int i=1; i<n; ++i) fa[i]=read();
for(int i=1; i<n; ++i) ++deg[i], ++deg[fa[i]];
for(int i=1; i<n; ++i) if(deg[i]==1) del(i, i);
for(int i=1; i<n-1; ++i) ans^=1ll*i*p[i];
}else{
for(int i=1; i<n-1; ++i) p[i]=read(), ++deg[p[i]];
for(int i=1; i<n; ++i) if(!deg[i]) add(i, i);
for(int i=1; i<n; ++i) ans^=1ll*i*fa[i];
}
printf("%lld\n", ans);
return 0;
}
基础的应用/例题
\(Prufer\) 序列有着远超你想象的强大功能。
看一道板子题 :Luogu P2290 [HNOI2004]树的计数
题目大意:给定每个点的度数,求有多少棵符合条件的树。
如果学过 \(Prufer\) 序列,那这就是一道一眼题。
由于上述 \(Prufer\) 序列的性质,每个点在序列中出现的次数等于点的度数-1, 题目已经给出了每个点的度数,那只需再求一次多重排列数,找出有多少种满足条件的序列即可。
最终答案:
\(\displaystyle ans= \frac{(n-2)!}{\prod_{i=1}^n deg_i -1}\)
code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=160;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, sum[N], deg[N];
long double ans=1;
signed main(void){
n=read();
for(int i=1; i<=n; ++i) deg[i]=read()-1;
for(int i=1; i<=n; ++i)
if(deg[i]>n-2) { puts("0"); return 0; }
for(int i=1; i<=n; ++i)
if(deg[i]) ++sum[deg[i]], sum[0]+=deg[i];
for(int i=n; i; --i) sum[i]+=sum[i+1];
if(sum[0]!=n-2) { puts("0"); return 0; }
for(int i=2; i<=n-2; ++i) ans*=pow(i, 1-sum[i]);
printf("%.0Lf\n", ans);
return 0;
}
另一道稍有难度的例题 :CF156D Clues
题目大意 :给定一张有 \(k\) 个联通块组成的图,第 \(i\) 个连通块大小为 \(s_i\) ,求添加 \(k-1\) 条边使得图联通的方案数。
不难想到先枚举每个点的度数 \(d_i\)。若一条边要连到一个连通块上,可以与连通块上的任何一个点相连,这样每个联通块内的情况有 :\(s_{i}^{d_{i}}\) 种情况。
再把每个连通块想象成一个点,考虑连成的树的情况,则总情况数为 :
\(\displaystyle ans=\sum_{d_i>0, \sum_{i=1}^{k}d_i=2k-2} \binom{k-2}{d_{1}-1, d_{2}-1, ... ,d_{k}-1} \prod_{i=1}^{k}s_{i}^{d_{i}}\)
这个式子乍一看就不是很好求(因为要枚举每种可能的 \(d\) ),因此考虑化简,发现这个式子和二项式定理扩展到多项式中的形式很像 :
\(\displaystyle (a_1+a_2+...+a_n)^t= \sum_{d_i\ge0, \sum_{i=1}^{n}d_i=t} \binom{t}{d_1, d_2, ... ,d_n} \prod_{i=1}^{n} a_i^{d_i}\)
因此原式可化成 :
\(\displaystyle ans=\prod_{i=1}^k s_{i} \sum_{d_i>0, \sum_{i=1}^{k}d_i=2k-2} \binom{k-2}{d_{1}-1, d_{2}-1, ... ,d_{k}-1} \prod_{i=1}^k s_{i}^{d_{i}-1}\)
\(\displaystyle ans=\prod_{i=1}^k s_{i} (\sum_{i=1}^k s_{i})^{k-2}\)
\(\displaystyle ans=\prod_{i=1}^k s_{i} \ n^{k-2}\)
于是 \(O(n)\) 求出这个式子即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
inline int read(){
int f=1, x=0; char ch=getchar();
while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
while(isdigit(ch)) { x=x*10+ch-48; ch=getchar(); }
return f*x;
}
int n, m, p, ans, s[N], top;
inline int qp(int n, int m){
int ans=1;
while(m){
if(m&1) ans=1ll*ans*n%p;
m>>=1; n=1ll*n*n%p;
}
return ans;
}
int fa[N], siz[N];
int find(int x) { return fa[x]==x ? x : fa[x]=find(fa[x]); }
int main(void){
n=read(), m=read(), p=read();
int x, y;
if(p==1) { puts("0"); return 0; }
for(int i=1; i<=n; ++i) fa[i]=i;
for(int i=1; i<=m; ++i){
x=read(), y=read();
if(find(x)==find(y)) continue;
fa[find(x)]=find(y);
}
for(int i=1; i<=n; ++i) ++siz[find(i)];
for(int i=1; i<=n; ++i) if(fa[i]==i) s[++top]=siz[i];
if(top==1) { puts("1"); return 0; }
ans=qp(n, top-2);
for(int i=1; i<=top; ++i)
ans=1ll*ans*s[i]%p;
printf("%d\n", ans);
return 0;
}