Prufer 序列学习笔记
Suffix_Automaton · · 题解
一、前言
主要是用于计数问题。
前置知识:树的定义。
二、定义
对于一棵有
-
选择其编号最小的度数为
1 的节点,输出唯一与其相邻的节点的编号。 -
删除这个节点以及以其为端点的唯一边。
然后这个东西有一些性质:
- 容易知道最后树上一定是只留下
2 个度数为1 的节点以及连着它们的边,其中一个编号为n 。
证明:将无根树视作以
- 这个东西可以与一棵有编号的无根树一一对应。
这个性质可以通过给出一种一一对应的映射方式来证明,详见下文。
- 每个节点在 Prufer 序列里出现的次数是它在无根树上的度数减一,特别地,一开始的所有叶子节点不会出现在 Prufer 序列里。
证明:每往 Prufer 序列中加一个节点,就会删掉一条边。最后每个节点都至多只有一条边。我们知道因为每次删的都是叶子节点,所以每删去一个节点以及以其为端点的唯一边后,无根树仍连通。具体来说,如果我们将树视为以
三、用途
-
可以知道有
n 个节点的不同有标号无根树一共有n^{n-2} 棵,即有n 个点的完全图的不同生成树一共有n^{n-2} 棵(Cayley 定理)。这是因为它的长为n-2 的 Prufer 序列的每一位都可以填1 到n 的任意正整数。有n 个节点的不同有标号有根树一共有n^{n-1} 棵,因为处理出无根树之后,n 个点都可以做根。 -
可以知道有
n 个节点,其中编号为i 的节点的度数为d_i ,并且满足\sum\limits_{i=1}\limits^n{d_i}=2\times n-2 的不同有标号无根树有\frac{(n-2)!}{\prod\limits_{i=1}\limits^n{(d_i-1)!}} 个。注意分母是先做阶乘再连乘。这是因为这棵无根树的 Prufer 序列可看作d_1-1 个1 、d_2-1 个2 、……、d_n-1 个n 所组成的,长度为n-2 的可重复元素的全排列。
四、例题
例 1.P6086 【模板】Prufer 序列
这一题是要实现
O(n\log n) 做法
首先实现父亲序列转 Prufer。一个明显的想法是记录所有点的子节点个数,维护一个小根堆(优先队列),把所有子节点个数为
然后就是 Prufer 序列转父亲序列,还是找出每一个节点的子节点数数(即在序列中出现的次数),然后找到所有子节点数为
所以就可以实现在
int main(){
n=read();m=read();
if(m==1){
for(int i=1;i<=n-1;i++)d[f[i]=read()]++;
for(int i=1;i<=n;i++)if(!d[i])q.push(i);
for(int i=1;i<=n-2;i++){
p[i]=f[q.top()];q.pop();
if(!--d[p[i]])q.push(p[i]);
}
for(int i=1;i<=n-2;i++)ans^=1ll*i*p[i];
}else{
for(int i=1;i<=n-2;i++)d[p[i]=read()]++;
for(int i=1;i<=n-1;i++)f[i]=n;
for(int i=1;i<=n;i++)if(!d[i])q.push(i);
for(int i=1;i<=n-2;i++){
f[q.top()]=p[i];q.pop();
if(!--d[p[i]])q.push(p[i]);
}
for(int i=1;i<=n-1;i++)ans^=1ll*i*f[i];
}
cout<<ans<<'\n';
return 0;
}
AC 记录,通过了就很神奇,直接
O(n) 做法
但是这一题的正解是
我们可以发现,这一次删了一个点,那么下一次删哪个点。首先,有可能是原本就是度数为
所以我们可以维护一个指针,初始时为
把 Prufer 序列转换成父亲序列同理。
int main(){
n=read();m=read();
if(m==1){
for(int i=1;i<=n-1;i++)d[f[i]=read()]++;
for(int i=1,j=1,x;i<=n-2;i++,j++){
while(d[j])j++;p[i]=f[x=j];
while(i<=n-2&&!--d[p[i]]&&p[i]<j)p[++i]=f[x=f[x]];
}
for(int i=1;i<=n-2;i++)ans^=1ll*i*p[i];
}else{
for(int i=1;i<=n-2;i++)d[p[i]=read()]++;p[n-1]=n;
for(int i=1,j=1,x;i<=n-1;i++,j++){
while(d[j])j++;f[x=j]=p[i];
while(i<=n-1&&!--d[p[i]]&&p[i]<j)f[x=f[x]]=p[++i];
}
for(int i=1;i<=n-1;i++)ans^=1ll*i*f[i];
}
cout<<ans<<'\n';
return 0;
}
AC 记录
然后这个东西有什么用呢?基本没用,毕竟现有的大部分 OI 题只考计数,不考求 Prufer 序列。但是万一考了呢?学有余力的话还是学一学吧。
例 2.P4981 父子
这一题就是求一个有
例 3.P4430 小猴打架
这一题就是求一个有
例 4.P2290 [HNOI2004]树的计数
使用上面(“用途”中)所讲的公式,可以知道是
例 5.P2624 [HNOI2008]明明的烦恼
考虑 Prufer 序列。令