Prufer序列(知识总结+板子整理)

思路来源

https://www.cnblogs.com/zwfymqz/p/8869956.html

https://www.luogu.com.cn/blog/xht37/solution-p6086

https://www.luogu.com.cn/blog/hekaiyu/prufer-xu-lie

知识总结

常用于生成树计数,毕竟都搞出序列了。

一个长度为n-2的Prufer序列,唯一对应一棵n个点固定形态的无根树。

 

T->P(Tree->Prufer)

  1. 找到编号最小的叶子x
  2. 设与叶子x相连的点是y,则删掉x,并在prufer序列尾部加入一个数y
  3. 重复1,2操作,直到整棵树只剩下两个节点,此时prufer序列长度为n-2

prufer

借鉴一下洛谷用户@hekaiyu的图片,是Tree->Prufer序列的流程

不难发现,剩的两个节点里一定会有一个是n,

最大的根本不会被优先删掉,所以一般用以n为根的有根树和Prufer序列互相转化比较方便

 

O(nlogn):

用堆或set维护一下

O(n):

先找到编号最小的叶节点。

每次删掉它之后,如果产生了新的叶节点且编号比指针指向的更小,则直接继续删掉,

否则自增找到下一个编号最小的叶节点。

 

P->T(Prufer->Tree)

  1. 把prufer序列的数加入队列中,设当前队首为u
  2. 维护一个还没有连边的点集set(初始放入1-n),找到点集其中编号最小的,且没有在当前队列中出现的元素v
  3. 给u,v连边,并在点集里删掉v,在队列里弹出u
  4. 最终,在点集set中剩下两个节点,给它们之间连一条边

其实就是刚才顺序构造,但是变加边为删边的过程,

考虑原树转prufer序列时,每次撸掉一个最小的叶子,

prufer转回原树的时候就找到最小的叶子接回去,并删掉对应的度数

后来就是接树枝的过程,但由于在树中加边的过程中减掉了对应的度数,

在这个过程中,参与接树枝的x此时度数已经为0了

 

O(nlogn):

用堆或set维护一下

O(n):

具体而言,指针指向编号最小的度数为 1 的节点。

每次将它与当前枚举到的 Prufer 序列的点连接之后,如果产生了新的度数为 1 的节点且编号比指针指向的更小,

则直接继续将它与下一个 Prufer 序列的点连接,否则自增找到下一个编号最小的度数为 1 的节点。

 

Prufer序列性质

  1. prufer序列中,点u出现的次数,等于点u在树中的度数-1

  2. n个点的无根树,唯一对应长度为n-2的prufer序列,序列每个数都在1到n的范围内。

  3. Cayley定理:n个点的无向完全图的生成树的计数:n^{n-2},即n个点的有标号无根树的计数

  4. n个节点的度依次为d1,d2,\dots,dn的无根树共有\frac{(n-2)!}{ \prod_{i=1}^n(d_i-1)!}个,因为此时Prufer编码中的数字i恰好出现di−1次,(n−2)!是总排列数

  5. n个点的有标号有根树的计数:n^{n-2}*n=n^{n-1}

一点个人的理解

1.分有没有被删讨论一下,

被删的点被删的时候还连有一个点,加入prufer序列的点不是自己;

没被删的那两个点,在树中都还剩一个度,其余的度在prufer序列里都是自己

2.显然

3.prufer序列任意位置都有[1,n]共n种选择,任意填法都对应一棵无根树

4.已知要填哪些数后,(n-2)!除掉每个内部的顺序(di-1)!

5.给一棵树指定根,共n种方法

 

模板题 P6086 【模板】Prufer 序列

请实现 Prufer 序列和无根树的相互转化。

为方便你实现代码,尽管是无根树,我们在读入时仍将 n(n<=5e6) 设为其根。

对于一棵无根树,设 f_{1\dots n-1}为其父亲序列(fi​ 表示 i 在 n 为根时的父亲),设p_{1 \dots n-2}为其 Prufer 序列

另外,对于一个长度为 m 的序列 a_{1 \dots m}​,我们设其权值为 \operatorname{xor}_{i = 1}^m i \times a_i

输入格式

第一行两个整数 n,m,表示树的点数和转化类型。

若 m = 1,第二行一行 n-1 个整数,表示父亲序列。

若 m = 2,第二行一行 n-2 个整数,表示 Prufer 序列。

输出格式

若 m = 1,一行一个整数,表示给出的父亲序列对应的 Prufer 序列的权值。

若 m = 2,一行一个整数,表示给出的 Prufer 序列对应的父亲序列的权值。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e6 + 7;
int n, o, f[N], p[N], d[N];
ll ans;
//实际构造时,把其看成以n为根的有根树更方便
//度指的是儿子->父亲 父亲+入度
//Tree->Prefer
inline void TP() {
    for (int i = 1; i < n; i++) scanf("%d",&f[i]),++d[f[i]];
    for (int i = 1, j = 1; i <= n - 2; i++, j++) {
        while (d[j]) ++j;//找到编号最小的叶节点
        p[i] = f[j];
        while (i <= n - 2 && !--d[p[i]] && p[i] < j) p[i+1] = f[p[i]], ++i;//新叶节点且编号比当前叶节点更小
    }
    for (int i = 1; i <= n - 2; i++) ans ^= 1ll * i * p[i];
}
//Prufer->Tree
inline void PT() {
    for (int i = 1; i <= n - 2; i++) scanf("%d",&p[i]), ++d[p[i]];//出现次数=度-1 度=0为叶
    p[n-1] = n;//最大的一定是最后两个点其一
    for (int i = 1, j = 1; i < n; i++, j++) {
        while (d[j]) ++j; f[j] = p[i];//把最小的叶往prufer序列第一个点上接 对应减掉度数
        while (i < n && !--d[p[i]] && p[i] < j) f[p[i]] = p[i+1], ++i;//如果序列第一个点减掉度数后产生了新的更小的叶 就往序列下一个点上接
    }
    for (int i = 1; i < n; i++) ans ^= 1ll * i * f[i];
}

int main() {
    scanf("%d%d",&n,&o), o == 1 ? TP() : PT(), printf("%lld\n",ans);
    return 0;
}

模板题 P2290 [HNOI2004]树的计数

一个有n(1<=n<=150)个节点的树,设它的节点分别为1-n,第i个节点的度数为di​,

问满足这样的条件的不同的树有多少棵,保证满足条件的树不超过1e17个。

 

①特判n=1 ②特判di-1之和是不是等于n-2 ③特判是否存在度数为0的孤立点使树不连通

代公式,\frac{(n-2)!}{ \prod_{i=1}^n(d_i-1)!},可以理解为n-2个位置,依次放入n种数第i种数有di-1个

分步放,选出一些位置放di-1,然后再放下一个,中间结果就不会爆ll了

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=155;
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
int n,sum,d[N];
ll c[N][N];
void init(){
    rep(i,0,N-2){
        c[i][0]=1;
    }
    rep(i,1,N-2){
        rep(j,1,i){
            c[i][j]=c[i-1][j-1]+c[i-1][j];
        }
    }
}
int main(){
    scanf("%d",&n);
    rep(i,1,n){
        scanf("%d",&d[i]);
        sum+=d[i]-1;
        if(!d[i] && n!=1){//判存在孤立点的情形
            return printf("0"),0;
        }
    }
    if(n==1){//判一个点的情形
        return printf("%d",d[1]==0),0;
    }
    if(sum!=n-2){//判prufer序列出现总次数
        return printf("%d",0),0;
    }
    init();
    ll ans=1;
    //n-2个位置 依次放入若干种di-1个相同的数 可以分步放 这样就不会爆ll了
    rep(i,1,n){
        ans=ans*c[sum][d[i]-1];
        sum-=d[i]-1;
    }
    printf("%lld",ans);
    return 0;
}

模板题 P4430 小猴打架

一开始森林里面有N(N<=1e6)只互不相识的小猴子,它们经常打架,但打架的双方都必须不是好朋友。

每次打完架后,打架的双方以及它们的好朋友就会互相认识,成为好朋友。

经过N-1次打架之后,整个森林的小猴都会成为好朋友。

现在的问题是,总共有多少种不同的打架过程,输出方案数mod 9999991。

比如当N=3时,就有{1-2,1-3}{1-2,2-3}{1-3,1-2}{1-3,2-3}{2-3,1-2}{2-3,1-3}六种不同的打架过程。

 

由Cayley定理,n^{n-2}次方种树形,

对于一种树形,n-1条边的加边顺序对应了不同的打架过程,再乘(n-1)!

n = int(input())
mod =9999991
ans =1
for i in range(n-2):
    ans = ans * n % mod
for i in range(1,n):
    ans = ans * i % mod
print(ans)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章