【知识】Prüfer 编码

Prüfer 序列

Prufer 序列可以将一个带标号 n 个节点的树用 [1,n] 中的 n2 个整数表示,即 n 个点的完全图的生成树与长度为 n2,值域为 [1,n] 的数列构成的双射。

Prufer 序列可以方便地解决一类树相关的计数问题,比如 凯莱定理nn2 个点的完全图的生成树有 nn2 个。

对树构造 Prüfer 序列

Prufer 是这样构造的:

每次选择一个编号最小的叶节点并删掉它,然后在序列中记录下它连接到的那个节点。

重复 n2 次后就只剩下两个节点,算法结束。

  • O(nlogn) 显然,使用堆可以做到 O(nlogn) 的复杂度。

  • O(n) 使用一个指针代替堆找最小值,可以做到 O(n) 的复杂度。

具体而言,指针指向编号最小的叶节点。每次删掉它之后,如果产生了新的叶节点且编号比指针指向的更小,则直接继续删掉,否则自增找到下一个编号最小的叶节点。

Prufer 序列的性质

从上述构造 Prüfer 序列的过程可以看出 Prüfer 序列具有以下两个性质:

  1. 在构造完 Prüfer 序列后,原树中会剩下两个节点,其中一个一定是编号最大的点 n
  2. 每个节点在序列中出现的次数是其度数减 1,因此没有出现的就是叶节点。

用 Prufer 序列构造树

根据 Prufer 序列的性质,我们可以得到原树上每个点的度数。

每次我们选择一个编号最小的度数为 1 的节点,与当前枚举到的 Prufer 序列的点连接,然后同时减掉两个点的度数。重复 n2 次后就只剩下两个度数为 1 的节点,其中一个是 n,把它们连接起来即可。

  • O(nlogn) 同样地,显然,使用堆可以做到 O(nlogn) 的复杂度。

  • O(n) 类似地,使用一个指针代替堆找最小值,可以做到 O(n) 的复杂度。

具体而言,指针指向编号最小的度数为 1 的节点。每次将它与当前枚举到的 Prufer 序列的点连接之后,如果产生了新的度数为 1 的节点且编号比指针指向的更小,则直接继续将它与下一个 Prüfer 序列的点连接,否则自增找到下一个编号最小的度数为 1 的节点。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int f[N], d[N], p[N];

void tree2prufer()
{
    for (int i = 1; i < n; i ++ )
    {
        scanf("%d", &f[i]);
        d[f[i]] ++ ;
    }

    for (int i = 0, j = 1; i < n - 2; j ++ )
    {
        while (d[j]) j ++ ;
        p[i ++ ] = f[j];
        while (i < n - 2 && -- d[p[i - 1]] == 0 && p[i - 1] < j) p[i ++ ] = f[p[i - 1]];
    }

    for (int i = 0; i < n - 2; i ++ ) printf("%d ", p[i]);
}

void prufer2tree()
{
    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; 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 ++ ;
    }

    for (int i = 1; i <= n - 1; i ++ ) printf("%d ", f[i]);
}

int main()
{
    scanf("%d%d", &n, &m);
    if (m == 1) tree2prufer();
    else prufer2tree();

    return 0;
}

Cayley 公式 (Cayley's formula)

完全图 Knnn2 棵生成树。

怎么证明?方法很多,但是用 Prüfer 序列证是很简单的。任意一个长度为 n2 的值域 [1,n] 的整数序列都可以通过 Prüfer 序列双射对应一个生成树,于是方案数就是 nn2

图连通方案数

Prüfer 序列可能比你想得还强大。它能创造比 凯莱公式 更通用的公式。比如以下问题:

一个 n 个点 m 条边的带标号无向图有 k 个连通块。我们希望添加 k1 条边使得整个图连通。求方案数。

证明

si 表示每个连通块的数量。我们对 k 个连通块构造 Prüfer 序列,然后你发现这并不是普通的 Prüfer 序列。因为每个连通块的连接方法很多。不能直接淦就设啊。于是设 di 为第 i 个连通块的度数。由于度数之和是边数的两倍,于是 i=1kdi=2k2。则对于给定的 d 序列构造 Prüfer 序列的方案数是

(k2d11,d21,,dk1)=(k2)!(d11)!(d21)!(dk1)!

对于第 i 个连通块,它的连接方式有 sidi 种,因此对于给定 d 序列使图连通的方案数是

(k2d11,d21,,dk1)i=1ksidi

现在我们要枚举 d 序列,式子变成

di1i=1kdi=2k2(k2d11,d21,,dk1)i=1ksidi

好的这是一个非常不喜闻乐见的式子。但是别慌!我们有多元二项式定理:

(x1++xm)p=ci0, i=1mci=p(pc1,c2,,cm)i=1mxici

那么我们对原式做一下换元,设 ei=di1,显然 i=1kei=k2,于是原式变成

ei0i=1kei=k2(k2e1,e2,,ek)i=1ksiei+1

化简得到

(s1+s2++sk)k2i=1ksi

nk2i=1ksi

证毕!

posted @   Star_F  阅读(48)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示