解题报告(八) prufer 序列与 Cayley 公式(ACM / OI)超高质量题解

繁凡出品的全新系列:解题报告系列 —— 超高质量算法题单,配套我写的超高质量题解和代码,题目难度不一定按照题号排序,我会在每道题后面加上题目难度指数( 1 ∼ 5 1 \sim 5 15),以模板题难度 1 1 1 为基准。


这样大家在学习算法的时候就可以执行这样的流程:

%
阅读我的【学习笔记】 / 【算法全家桶】学习算法 ⇒ \Rightarrow 阅读我的相应算法的【解题报告】获得高质量题单 ⇒ \Rightarrow 根据我的一句话题解的提示尝试自己解决问题 ⇒ \Rightarrow 点开我的详细题解链接学习巩固(好耶)

%
解题报告系列合集:【解题报告系列】超高质量题单 + 题解(ICPC / CCPC / NOIP / NOI / CF / AT / NC / P / BZOJ)

本题单前置知识:【学习笔记】树的计数,prufer(Prüfer)编码,Cayley公式及相应例题

1. prufer \text{prufer} prufer 序列

Prufer数列是无根树的一种数列。在组合数学中,Prufer数列由有一个对于顶点标过号的树转化来的数列,点数为n的树转化来的Prufer数列长度为n-2。

2. prufer \text{prufer} prufer 序列的性质

性质1: prufer \text{prufer} prufer 序列与无根树一一对应。

显然

性质2: 度数为 d i d_i di 的节点会在 prufer \text{prufer} prufer 序列中出现 d i − 1 d_i-1 di1 次。

当某个节点度数为 1 1 1 时,会直接被删掉,否则每少掉一个相邻的节点,它就会在序列中出现 1 1 1 次。

因此共出现 d i − 1 d_i-1 di1 次。

性质3: 一个 n 个节点的完全图的生成树个数为 n n − 2 n^{n-2} nn2 (Cayley 公式)

对于一个 n n n 个点的无根树,它的 prufer \text{prufer} prufer 序列长为 n − 2 n-2 n2 ,而每个位置有 n n n 种可能性,因此可能的 prufer \text{prufer} prufer 序列有 n n − 2 n^{n-2} nn2 种。

又由于 prufer \text{prufer} prufer 序列与无根树一一对应,因此生成树个数应与 prufer \text{prufer} prufer 序列种树相同,即 n n − 2 n^{n-2} nn2

性质4: 对于给定度数为 d 1 ∼ n d_{1\sim n} d1n 的一棵无根树共有 ( n − 2 ) ! ∏ i = 1 n ( d i − 1 ) ! \cfrac{(n-2)!}{\prod_{i=1}^n(d_i-1)!} i=1n(di1)!(n2)! 种情况。

由上面的性质可以知道,度数为 d i d_i di 的节点会在 prufer \text{prufer} prufer 序列中出现 d i − 1 d_i-1 di1 次。

则就是要求出 d i − 1 d_i-1 di1 i ( 1 ≤ i ≤ n ) i(1\le i\le n) i(1in)的全排列个数。

而上面那个式子就是可重全排列公式。(即全排列个数除以重复元素内部的全排列个数)

3. Cayley \text{Cayley} Cayley 公式的推论

  1. n n n 个无序点的有根树个数为 n n − 1 n^{n-1} nn1

  2. n n n 个无序点的无根树个数为 n n − 2 n^{n-2} nn2

  3. n n n 个有序点的有根树个数为 n n − 1 × ( n − 1 ) ! n^{n-1}\times (n-1)! nn1×(n1)!

  4. n n n 个有序点的无根树个数为 n n − 2 × ( n − 1 ) ! n^{n-2}\times (n-1)! nn2×(n1)!

A、(P6086 【模板】)Prufer 序列

Weblink

https://www.luogu.com.cn/problem/P6086

Problem

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

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

对于一棵无根树,设 f 1 … n − 1 f_{1\dots n-1} f1n1 为其父亲序列( f i f_i fi 表示 i 在 n 为根时的父亲),设 p 1 … n − 2 p_{1 \dots n-2} p1n2 为其 Prufer 序列。

另外,对于一个长度为 m 的序列 a 1 … m a_{1 \dots m} a1m ,我们设其权值为 xor ⁡ i = 1 m i × a i \operatorname{xor}_{i = 1}^m i \times a_i xori=1mi×ai

Solution

由无根树得到其 prufer 序列:

找到编号最小的度数为1的点
删除该节点并在序列中添加与该节点相连的节点的编号
重复1,2操作,直到整棵树只剩下两个节点

双指针 O ( n ) O(n) O(n) 实现: 指针指向编号最小的叶节点。每次删掉它之后,如果产生了新的叶节点且编号比指针指向的更小,则直接继续删掉,否则自增找到下一个编号最小的叶节点。

由 prufer 序列还原该无根树:

每次取出prufer序列中最前面的元素u
在点集中找到编号最小的没有在prufer序列中出现的元素v
给u,v连边然后分别删除
最后在点集中剩下两个节点,给它们连边

双指针 O ( n ) O(n) O(n) 实现: 指针指向编号最小的度数为 1 的节点。每次将它与当前枚举到的 Prufer 序列的点连接之后,如果产生了新的度数为 1 的节点且编号比指针指向的更小,则直接继续将它与下一个 Prufer 序列的点连接,否则自增找到下一个编号最小的度数为 1 的节点。

Code

const int N = 6e6 + 7, mod = 1e9 + 7;

int n, m, t;
int p[N], f[N], d[N];
ll ans = 0;

void TtoP()
{
    
    
    for(int i = 1; i <= n - 1; ++ i) {
    
    
        scanf("%d", &f[i]);
        d[f[i]] ++ ;
    }
    for(int i = 1, j = 1; i <= n - 2; ++ i, ++ j) {
    
    
        while(d[j]) ++ j;//从小到大找到第一个度数为 1 (d[j] == 0) 的结点
        p[i] = f[j];//将父结点放入 prufer 序列里
        while(i <= n - 2 && -- d[p[i]] == 0 && p[i] < j)
            p[i + 1] = f[p[i]], ++ i;
    }
    ans = 1ll * p[1];
    for(int i = 2; i <= n - 2; ++ i) {
    
    
        ans ^= 1ll * i * p[i];
    }
}

void PtoT()
{
    
    
    for(int i = 1; i <= n - 2; ++ i) {
    
    
        scanf("%d", &p[i]);
        d[p[i]] ++ ;
    }
    p[n - 1] = n;
    for(int i = 1, j = 1; i <= n - 1; ++ 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;
    }
    ans = 1ll * f[1];
    for(int i = 2; i <= n - 1; ++ i)
        ans ^= 1ll * i * f[i];
}

int main()
{
    
    
    scanf("%d%d", &n, &m);
    if(m == 1) TtoP();
    else PtoT();
    printf("%lld\n", ans);
    return 0;
}

B、(P2290 [HNOI2004])树的计数

Weblink

https://www.luogu.com.cn/problem/P2290

Problem

一个有 n n n 个节点的树,设它的节点分别为 v 1 , v 2 , … , v n v_1,v_2,\ldots,v_n v1,v2,,vn ,已知第 i i i 个节点 v i v_i vi 的度数为 d i d_i di ,问满足这样的条件的不同的树有多少棵。

Solution

性质4: 对于给定度数为 d 1 ∼ n d_{1\sim n} d1n 的一棵无根树共有 ( n − 2 ) ! ∏ i = 1 n ( d i − 1 ) ! \cfrac{(n-2)!}{\prod_{i=1}^n(d_i-1)!} i=1n(di1)!(n2)! 种情况。我们可以直接用公式求解。但是这个公式可能会爆 long long ,不想写高精,所以考虑另一种思路。

我们发现题目其实可以转化成一共有 n − 2 n-2 n2 个位置,要放 n n n 个数,其中数 i i i 要占 d [ i ] − 1 d[i]-1 d[i]1 个位置。显然答案为: a n s   = ∏ i = 1 n   C d [ i ] − 1 s u m ans \ = \prod_{i=1}^n\ C_{d[i] - 1} ^ {sum} ans =i=1n Cd[i]1sum ,其中 s u m sum sum 为剩余的位置数。

最后需要特判一下不合法的情况:

  1. 图不合法,即有点的度数为 0 0 0 ,说明不是连通图,也就不是树,输出 0 0 0

  2. prufer序列不合法,即 ∑ d i − 1 ! = n − 2 \sum d_i-1!=n-2 di1!=n2

  3. n = 1 n=1 n=1 ,当 d 1 = 0 d_1=0 d1=0 合法,输出 1,否则不合法,输出 0

Code

const int N = 507;

ll c[N][N];
int n, m;
ll d[N], sum;

void solve()
{
    
    
    scanf("%d", &n);
    if(n == 1) {
    
    
        int d;
        scanf("%lld", &d);
        if(d == 0)
            puts("1");
        else puts("0");
        return ;
    }
    bool flag = 0;
    for(int i = 1; i <= n; ++ i) {
    
    
        scanf("%lld", &d[i]);
        if(d[i] == 0) flag = 1;
        sum += d[i] - 1;
    }
    if(sum != n - 2 || flag) {
    
    
        puts("0");
        return ;
    }
    c[0][0] = c[1][0] = c[1][1] = 1;
    for(int i = 2; i <= sum; ++ i) {
    
    
        c[i][0] = 1;
        for(int j = 1; j <= sum; ++ j) {
    
    
            c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
        }
    }
    ll ans = 1;
    for(int i = 1; i <= n; ++ i) {
    
    
        ans *= c[sum][d[i] - 1];
        sum -= d[i] - 1;
    }
    printf("%lld\n", ans);
    return ;
}

int main()
{
    
    
    solve();
}

C、(CF156D) Clues

Weblink

https://www.luogu.com.cn/problem/CF156D

Problem

给定一个 n n n 个点 m m m 条边的带标号无向图,它有 k k k 个连通块,求添加 k − 1 k-1 k1 条边使得整个图连通的方案数,答案对 p p p 取模。

Solution

结论: a n s = n k − 2 × ∏ i = 1 k s i \displaystyle ans=n^{k-2}\times \prod_{i=1}^ks_i ans=nk2×i=1ksi

其中 s i s_i si 为第 i i i 个连通块的点数。

我们直接用并查集计算一下连通块的点的数量即可。

证明:

在这里插入图片描述
Code

const int N = 1e5 + 7, mod = 1e9 + 7;
int n, m, p, k;
int fa[N];
int s[N];

int qpow(int a, int b)
{
    
    
    int res = 1;
    while(b) {
    
    
        if(b & 1) res = 1ll * res * a % p;
        a = 1ll * a * a % p;
        b >>= 1;
    }
    return res;
}

int Find(int x)
{
    
    
    if(fa[x] == x) return x;
    return fa[x] = Find(fa[x]);
}

void Union(int x, int y)
{
    
    
    int fx = Find(x), fy = Find(y);
    fa[fx] = fy;
}

int main()
{
    
    
    scanf("%d%d%d", &n, &m, &p);
    if(p == 1) return puts("0"), 0;
    for(int i = 1; i <= n; ++ i)
        fa[i] = i;
    for(int i = 1; i <= m; ++ i) {
    
    
        int x, y;
        scanf("%d%d", &x, &y);
        Union(x, y);
    }
    for(int i = 1; i <= n; ++ i)
        s[Find(i)] ++ ;

    ll ans = 1;
    for(int i = 1; i <= n; ++ i)
        if(i == Find(i)) ++ k, ans = (ans * 1ll * s[i]) % p;
    if(k == 1) return puts("1"), 0;
    ans = (ans * 1ll * qpow(n, k - 2)) % p;
    printf("%lld\n", ans);
    return 0;
}

D、UVA10843 Anne’s game

Weblink

https://www.luogu.com.cn/problem/UVA10843

Problem

A n n e Anne Anne 喜欢玩一个游戏:

  • 她在一张纸上画一个圆
  • 然后再画一个圆,并用一条线将其与另一个圆连接起来
  • 接着再画一个圆,并用一条线将其与前两个圆中的任意一个连接起来
  • 重复上述操作,直至她画了 n n n 个圆,且每个圆都与先前绘制的任意一个圆连接,所有圆都不相交,且每一条线也不相交
  • 最后,她在这些圆中随机填入 1 1 1 ~ n n n 这些数字(每个圆仅填入一个数字)

那么, A n n e Anne Anne 可以得到多少种不同的图?
两幅图不同的条件:其中一幅图有一条线连接了编号为 i i i 和编号为 j j j 的圆,而另一幅图没有。

1 < n ≤ 100 1<n\le100 1<n100

Solution

随机填入编号 —— 无序点, n n n 个无序点的无根树个数为 n n − 2 n^{n-2} nn2

直接输出即可。

Code

const int N = 1e5 + 7, p = 2000000011;
int n, m, k, kcase, t;

int qpow(int a, int b)
{
    
    
    int res = 1;
    while(b) {
    
    
        if(b & 1) res = 1ll * res * a % p;
        a = 1ll * a * a % p;
        b >>= 1;
    }
    return res;
}

int main()
{
    
    
    scanf("%d", &t);

    while(t -- ) {
    
    
        scanf("%d", &n);
        if(n <= 2) {
    
    
            printf("Case #%d: 1\n", ++ kcase);
            continue;
        }
        printf("Case #%d: %d\n", ++ kcase, qpow(n, n - 2));
    }
    return 0;
}

E、(P4430) 小猴打架

Problem

一开始森林里面有N只互不相识的小猴子,它们经常打架,但打架的双方都必须不是好朋友。每次打完架后,打架的双方以及它们的好朋友就会互相认识,成为好朋友。经过N-1次打架之后,整个森林的小猴都会成为好朋友。 现在的问题是,总共有多少种不同的打架过程。 比如当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}六种不同的打架过程。

Solution

其实就是有 n n n 个结点,经过 n − 1 n-1 n1 次连边形成一棵树,问有多少种连边过程,不止是连边的方案数,最后生成的树的不同方案也算方案数。

首先显然 n n n 个无序点的无根树的方案数为 n n − 2 n^{n-2} nn2,并且有 n − 1 n-1 n1 次连边,也就是有 ( n − 1 ) ! (n-1)! (n1)! 种连边方案,总方案数为 ( n − 1 ) ! × n n − 2 (n-1)!\times n^{n-2} (n1)!×nn2

const int N = 1e5 + 7, mod = 9999991;
int n, m, ans;
int main()
{
    
    
    ans = 1;
    scanf("%d", &n);
    for(int i = 1; i <= n - 2; ++ i) ans = 1ll * ans * n % mod;
    for(int i = 1; i <= n - 1; ++ i) ans = 1ll * ans * i % mod;
    printf("%d\n", ans);
    return 0;
}

F、(BZOJ1005) 明明的烦恼

Problem

给出标号为 1 到 N 的点,以及某些点最终的度数,允许在任意两点间连线,可产生多少棵度数满足要求的树?

1 ≤ n ≤ 1000 1\le n\le 1000 1n1000

Solution

https://www.cnblogs.com/zhj5chengfeng/archive/2013/08/23/3278557.html