Prüfer 序列 学习笔记

前言

  • msjing一下午学了 \(4\) 个东西,现在非常有精神

这个紫题和我上午刷的真的是同一个难度吗
——msjing

Prüfer简介及操作

  • 这个玩意看起来好吊的样子,那这究竟是个啥呢

Prüfer可以将一个带标号 \(n\) 个结点的树用 \([1,n]\) 中的 \(n-2\) 个整数表示.你也可以把它理解为完全图的生成树与数列之间的双射,常用组合计数问题中。
——oiwiki

计算序列

  • 这样的话一个有标无根树就可以表示了,具体怎么操作呢
  • 每次选择一个编号最小的叶结点并删掉它,然后在序列中记录下它连接到的那个结点.重复 \(n-2\) 次后就只剩下两个结点,算法结束。
  • 我们来徒手模拟一下
  • c这个CS Academy怎么加载这么慢,算了画吧,欸你怎么加载好了

tu

  • 艹浏览器突然无了图啥的全没了
  • 请忽视字体和图大小不一和 HZ 保密画质
  • 我们有了这个序列之后,我们就可以还原这块树了,怎么还原呢

树的还原

  • 性质:
    • 每个结点在序列中出现的次数是其度数 \(-1\)
    • 没有出现的是叶子结点
  • 有了这个,我们就可以依据数列还原了
  • 我们选一个度数为 \(1\) 的编号最小的点,与当前枚举到的序列的点连接,然后同时减掉两个点的度,不断重复,直到最后剩下两个度数为 \(1\) 的点,把它们连上就行了,度用桶维护,给图(图中是先把 \(5\)\(7\) 连上了)

吐2

biao

  • 我们可以结合图与表清晰理解

线性构造序列和还原树

  • 构造的话,优先队列的堆做法比较显然,e,不准备胡了
  • 但是我们需要线性搞,所以我们来胡一下
  • 先胡树构建序列
瞎胡时间
  • 遍历找最小叶子节点,找到后把它父亲加入序列,父亲度 \(-1\)
  • 为了方便表述,把上面说的父亲设为 \(f\),遍历找的叶子节点的下标设为 \(i\)
  • 如果 \(f\) 成为了叶子结点,判断其与 \(i\) 的大小关系,如果 \(f<i\),就直接加入,并把 \(f\) 的父亲度 \(-1\),然后重复上述,否则就跳过就行
  • 由于每个点只会被遍历一次,所以是 \(O(n)\)
  • 貌似胡对了
  • 序列重建树差不多,msjing不打算胡了,咕咕咕
  • 代码msjing是 \(rand\) 了一个贺的,给吧
点击查看代码
#include<bits/stdc++.h>
#define pb push_back
#define int long long
using namespace std;
constexpr int maxn=5e6+10;
int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || ch>'9')
    {
        if (ch == '-') f=-1;
        ch=getchar();
    }
    while (ch>='0' && ch<='9')
    {
        x=(x<<1)+(x<<3)+ch-'0';
        ch=getchar();
    }
    return x*f;
}
int n,f[maxn],p[maxn],d[maxn];
struct pru
{
    void ttop()
    {
        for (int i=1,j=1;i<=n-2;i++,j++)
        {
            while (d[j]) j++;
            p[i]=f[j];
            d[f[j]]--;
            while (i<=n-2 && !d[p[i]] && p[i]<j) p[i+1]=f[p[i]],d[p[i+1]]--,i++;
        }
        int res=0;
        for (int i=1;i<=n-2;i++) res^=i*p[i];
        printf("%lld\n",res);
    }
    void ptot()
    {
        p[n-1]=n;
        for (int i=1,j=1;i<=n-1;i++,j++)
        {
            while (d[j]) j++;
            f[j]=p[i];
            d[f[j]]--;
            while (i<=n-1 && !d[p[i]] && p[i]<j) f[p[i]]=p[i+1],d[p[i+1]]--,i++;
        }
        int res=0;
        for (int i=1;i<=n-1;i++) res^=i*f[i];
        printf("%lld\n",res);
    }
}P;
signed main()
{
    n=read();
    int op=read();
    if (op == 1)
    {
        for (int i=1;i<=n-1;i++) f[i]=read(),d[f[i]]++;
        P.ttop();
    }
    else
    {
        for (int i=1;i<=n-2;i++) p[i]=read(),d[p[i]]++;
        P.ptot();
    }
    return 0;
}

性质

  • e,这个会随着刷题的增加慢慢补充
  • 对于给定度数为 \(d\) 的一棵无根树共有 \({(n-2)!} \over {\prod^{n}_{i=1}{(d_i - 1)!}}\) 种情况

例题

P2290 [HNOI2004] 树的计数

  • \(Python\) 创似所有高精!!!
  • e,给出所有点的度,让求方案数,其实是比较裸的 \(Prufer\),柿子就是上面性质的第一个(好像只有一个 on 26.7.20),套柿子搞一下就行了
  • 巴特!这个东西会爆 \(long\) \(long\),所以要上高精,但是msjing太咕了于是直接用 \(Python\) 创过去了
Python 创创创!
n=(int)(input())
if n == 1:
    x=(int)(input())
    if x == 0: print(1)
    else: print(0)
else:
    f=[0]*(n+10)
    f[0]=1
    for i in range(1,n+1):
        f[i]=f[i-1]*i
    ans=f[n-2]
    cnt=0
    s=input().split()
    for i in range(n):
        x=(int)(s[i])
        if x == 0:
            print(0);exit()
        ans//=f[x-1];cnt+=x-1
    if cnt == n-2:print(ans)
    else:print(0)

后话

  • msjing学的非常草率,所以就现这样吧(·W·)
posted @ 2026-07-19 19:55  msjing  阅读(14)  评论(1)    收藏  举报