Prüfer 序列 学习笔记

引入

Prüfer 序列可以用于求解序列与树的双射,常用于组合计数问题。

定义

Prüfer 序列指的是每次选取一个编号最小的叶子,删除它,然后在序列中记录它所链接的点,重复以上步骤直到只剩下两个节点。

过程

对树建立 Prüfer 序列

显然可以用堆实现一个朴素的 \(\mathcal{O}(n \log n)\) 算法。

但是,效率太低了,其实还存在一个线性的算法。

线性建立 Prüfer 序列

可以发现,每删除一个叶子节点,叶子节点的数量最大增加一个,即叶子节点的数量是非严格单调递减的。

那么可以维护一个指针 \(p\),指向当前编号最小的节点,执行以下操作:

  1. 删除当前节点 \(p\),然后检查是否出现新的叶子节点。
  2. 如果出现新的叶子节点 \(x\),如果 \(x < p\) 立刻将 \(x\) 删除,并执行 \(1\) 步骤,否则跳过不管。
  3. 重复以上操作,知道只剩下两个点。

正确性显然,因为每个点如果小于 \(p\),那么会立刻删除,否则之后肯定会访问到它。

复杂度分析,可以发现每条边最多删除一次,因此时间复杂度显然\(\mathcal{O}(n)\)

性质

  1. 根据上述过程,显然 \(n\) 号节点永远不可能删除。
  2. 每个点出现次数是其度数减一。

通过 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 序列,它的方案数有:

\[\frac{(k - 2) !}{\prod\limits_{i = 1} ^ n (d_i - 1) !} \]

然后对于每个联通块内部有 \(s_i^{d_i}\) 种方案(考虑没条边连向哪个点),还要枚举每个联通块的度数,因此总的方案有:

\[\sum\limits_{d_i > 0 , \sum\limits_{i = 1} ^ n d_i = 2k - 2}\frac{(k - 2) !}{\prod\limits_{i = 1} ^ n (d_i - 1) !} \prod\limits_{i = 1} ^ k s_i^{d_i} \]

这个式子貌似化简不了了,但是还有一个多元二项式定理可以化简它。

多元二项式定理

\[(x_1 + x_2 + \cdots + x_n) ^ m = \sum\limits_{c_i > 0,\sum\limits_{i = 1} ^ n c_i = m} \frac{m !}{\prod\limits_{i = 1} ^ n (c_i - 1) !} \prod_{i = 1} ^ n x_i ^ {c_i} \]

因此可以设 \(e_i = d_i - 1\),那么有 \(\sum e_i =k - 2\)

因此原式可以化为:

\[\sum\limits_{d_i > 0 , \sum\limits_{i = 1} ^ n e_i = k - 2}\frac{(k - 2) !}{\prod\limits_{i = 1} ^ n e_i !} \prod\limits_{i = 1} ^ k s_i^{e_i + 1} \]

可以化简为:

\[(s_1 + s_2 + \cdots + s_k) ^ {k - 2} \prod_{i = 1} ^ k s_i \]

也就是:

\[n^{k - 2} \prod_{i = 1} ^ k s_i \]

posted @ 2024-06-06 10:39  Z_drj  阅读(39)  评论(0编辑  收藏  举报