树的计数问题-Prufer 序列

树的计数问题-Prufer 序列

问题引入

P2290

设一颗树有 n n n个节点,分别是 v 1 , v 2 , … , v n v_{1},v_{2},\ldots,v_{n} v1,v2,,vn,其度分别为 d 1 , d 2 , … , d n d_{1},d_{2},\ldots,d_{n} d1,d2,,dn,问构成这样的无根树总共有多少种?

Prufer 序列

很明显,在一颗树上用排列组合定理是相当困难的,我们能不能把一颗无根树转换成对应的唯一一个序列,即让一个序列与一个 带标号的无根树(LUT) 一一对应,构成双射,然后就可以对线性的序列使用排列组合定理,对线性的序列使用排列组合定理这是容易的。答案就是Prufer 序列

带标号的无根树(LUT):我们对一颗节点数为 n n n的无根树对其节点进行标号,每一个节点都有唯一一个标号(从1-n),叫做带标号的无根树。

无根树转Prufer 序列

我们通过下列几个步骤将一颗无根树转换为Prufer 序列。

  1. 初始化 Prufer 序列为 { } \{\} {}
  2. 在树中选择一个节点,其度为 1 1 1,且节点索引尽量小。将这个节点的父节点加入到Prufer 序列中,并且删除这个节点。
  3. 一直重复下去,直到无根树中还剩两个节点。

此时生成的序列就是这颗无根树的Prufer 序列。并且这个序列的长度是 n − 2 n-2 n2

举例来说:

无根树

步数选择节点Prufer序列
142
252,3
332,3,1
462,3,1,2
522,3,1,2,1

最终得到的Prufer序列为 { 2 , 3 , 1 , 2 , 1 } \{2,3,1,2,1\} {2,3,1,2,1},树中的节点剩下1和7。这颗树就和序列 { 2 , 3 , 1 , 2 , 1 } \{2,3,1,2,1\} {2,3,1,2,1}一一对应。

Prufer 序列转无根树

Prufer 序列转无根树的过程类似于上述过程的逆过程,根据Prufer的性质,每个节点的出现次数都是度数-1,我们每次可以选取最小度数为1的节点:

  1. 初始化点集合 V V V为全体节点
  2. V V V中取一个点 u u u,满足 u u u不在Prufer 序列中且 u u u的索引最小。与当前Prufer 序列的第一个元素 v v v,在 u u u v v v之间添加一条边。并弹出Prufer 序列的首元素,并且在 V V V中删除节点 u u u
  3. 重复上述过程,直到Prufer 序列为空,此时 V V V还剩下两个节点,在这两个节点之间添加一条边,此时生成的树就是原来的无根树。

举例来说:

步数 V V V u u u v v vPrufer序列
11,2,3,5,6,7423,1,2,1
21,2,3,6,7531,2,1
31,2,6,7312,1
41,2,7621
51,721 ϕ \phi ϕ

最后添加边 1 → 7 1 \to 7 17结束,可见,最后一次添加的边一定是最大的节点和Prufer序列最后一个节点相连的边。

因此我们看,选择最小的元素是为了能和逆过程对应上,我们也可以选择其他条件,只要能和逆过程对应上即可。

模板

P6086

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
int fa[5010110];
int deg[5010110];
int prufer[5010110];

int n, m;

ll read()
{
    ll cnt = 0, flag = 1;
    char c = getchar();
    while (c < '0' || c > '9')
    {
        if (c == '-')
            flag = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
        cnt = (cnt << 1) + (cnt << 3) + (c ^ 48), c = getchar();
    return flag * cnt;
}

void prufer_code()
{
    // count degree
    for (int i = 1; i <= n - 1; i++)
    {
        deg[i]++;
        deg[fa[i]]++;
    }
    int ptr = -1;

    // find the node
    for (int i = 1; i <= n; i++)
    {
        if (deg[i] == 1)
        {
            ptr = i;
            break;
        }
    }

    int leaf = ptr;

    for (int i = 0; i < n - 2; i++)
    {
        prufer[i] = fa[leaf];
        deg[leaf]--;
        deg[fa[leaf]]--;
        if (deg[fa[leaf]] == 1 && fa[leaf] < ptr)
        {
            leaf = fa[leaf];
        }
        else
        {
            while (deg[ptr] != 1)
            {
                ptr++;
            }
            leaf = ptr;
        }
    }
}

void prufer_decode()
{
    // count degree

    for (int i = 0; i < n - 2; i++)
    {
        deg[prufer[i]]++;
    }

    for (int i = 1; i <= n; i++)
    {
        deg[i]++;
    }
    int ptr = -1;
    for (int i = 1; i <= n; i++)
    {
        if (deg[i] == 1)
        {
            ptr = i;
            break;
        }
    }

    int leaf = ptr;

    for (int i = 0; i < n - 2; i++)
    {
        fa[leaf] = prufer[i];

        deg[leaf]--;
        deg[prufer[i]]--;

        if (deg[prufer[i]] == 1 && prufer[i] < ptr)
        {
            leaf = prufer[i];
        }
        else
        {
            while (deg[ptr] != 1)
            {
                ptr++;
            }
            leaf = ptr;
        }
    }

    fa[prufer[n - 3]] = n;
}

int main()
{
    n = read();
    m = read();
    if (m == 1)
    {
        for (int i = 1; i <= n - 1; i++)
        {
            fa[i] = read();
        }
        prufer_code();
        long long ans = 0;
        for (int i = 0; i < n - 2; i++)
        {
            ans ^= (i + 1ll) * prufer[i];
        }
        printf("%lld", ans);
    }
    else
    {
        for (int i = 0; i < n - 2; i++)
        {
            prufer[i] = read();
        }

        prufer_decode();

        long long ans = 0;
        for (int i = 1; i <= n - 1; i++)
        {
            ans ^= ((long long)i) * fa[i];
        }
        printf("%lld", ans);
    }
    return 0;
}

Prufer 序列计数

有了Prufer 序列我们就可以排列组合了,我们借助如下几个性质进行计算:

  1. 无根树和Prufer 序列一一对应,构成双射。
  2. 度数为 d i d_{i} di的节点会在Prufer 序列出现 d i − 1 d_{i}-1 di1次,即为节点的子节点的个数。
  3. Cayley公式:一个完全图有 n n n个节点,其生成树的个数有 n n − 2 n^{n-2} nn2。Prufer 序列有 n − 2 n-2 n2个位置,每个位置都有 n n n种可能。
  4. 对于给定的节点的度,无根树的个数有:

A ( n − 2 ) ∏ i = 1 n A ( d i − 1 ) \frac{A(n-2)}{\prod_{i=1}^{n}A(d_{i}-1)} i=1nA(di1)A(n2)

即先对序列进行全排列,然后除以相同元素的全排列即可。

#include <bits/stdc++.h>
#define MAX_SIZE 1000
using namespace std;

typedef long long ll;

struct Decimal
{
    int fact[MAX_SIZE];
    Decimal()
    {
        for (int i = 0; i < MAX_SIZE; i++)
            fact[i] = 0;
    }
    void set(int num)
    {
        int fa = 2;
        while (num > 1)
        {
            int cnt = 0;
            while (num % fa == 0)
            {
                num /= fa;
                cnt++;
            }
            if (cnt != 0)
            {
                fact[fa] = cnt;
            }
            fa++;
        }
    }

    void operator*=(const Decimal &a)
    {
        for (int i = 0; i < MAX_SIZE; i++)
        {
            fact[i] += a.fact[i];
        }
    }

    void operator/=(const Decimal &a)
    {
        for (int i = 0; i < MAX_SIZE; i++)
        {
            fact[i] -= a.fact[i];
        }
    }
};

ostream &operator<<(ostream &out, const Decimal &d)
{
    ll ans = 1;
    for (int i = 0; i < MAX_SIZE; i++)
    {
        for (int cnt = 0; cnt < d.fact[i]; cnt++)
        {
            ans *= i;
        }
    }

    out << ans;
    return out;
}

int main()
{
    int n;
    cin >> n;
    // 特判n=1的情况
    if (n == 1)
    {
        int deg;
        cin >> deg;
        if (deg == 0)
            cout << "1";
        else
            cout << "0";
        return 0;
    }
    // 阶乘准备
    Decimal factorial[151];
    for (int i = 1; i <= n; i++)
    {
        factorial[i].set(i);
        factorial[i] *= factorial[i - 1];
    }
    // 初始化答案为(n-2)!
    Decimal ans = factorial[n - 2];

    int cnt = 0;

    for (int i = 0; i < n; i++)
    {
        int deg;
        cin >> deg;
        // 如果度为0,图不连图,不是树,无解
        if (deg == 0)
        {
            cout << "0";
            return 0;
        }
        // 计数,除掉相同元素的排列
        cnt += deg - 1;
        ans /= factorial[deg - 1];
    }
    // 判断是否成立
    if (cnt == n - 2)
    {
        cout << ans;
    }
    else
    {
        cout << "0";
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值