胖爷XP的hu测 T1.水晶(Prufer数列+dp)

119 篇文章 5 订阅
61 篇文章 0 订阅

版权属于XP,想要引用此题(包括题面)的朋友请联系博主

这里写图片描述
这里写图片描述

分析:
假如我们从 n n 个点钟选出了s个点,那么这 s s 个点的合法连通状态一定是一棵树

每个结点都有一个最大度数(也就是说每个结点连出的边数<=ai
<= <= <script type="math/tex" id="MathJax-Element-12"><=</script>的限制比较烦,我们先看 =ai = a i 的情况

问题转化成:已知s个点,每个点的度数 ai a i ,求能形成的不同形态树的数量

关于这个问题,有一个有力的工具:Prufer定理


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

Prufer数列的生成

找出这棵树的当前叶子结点中标号最小的那个,把与ta相连的点的编号加入 prufer p r u f e r 数列,然后删除这个叶子结点
操作到最后剩下两个结点时,这两个结点会被同时删除

举个例子:
这里写图片描述

Prufer数列转化成数

每次找到当前prufer数列中未出现的编号中最小的编号,把ta对应的点和当前prufer数列中开头的点连接,然后删除数列开头的点

Prufer数列的性质

  • 一棵n个结点的有编号无根树的prufer数列长度为n-2
    显然,每次删除一个结点就会在数列中加入一个数,直到剩最后两个结点的ta们同时被删除,数列中自然有n-2个数

  • 原树中结点度数为 di d i 的结点编号在prufer数列中出现 di1 d i − 1
    由于一个结点在作为叶子结点删除时,ta的当前度数为1,而每删除一个与ta相邻的叶子结点时,ta的编号会被计入一次,且ta的度数会-1,所以ta最终会被记录 di1 d i − 1 次,然后作为叶子结点被删除

  • 每个prufer数列都和一棵有编号无根树意义对应
    由于在对有标号无根树进行操作时,每一步的操作都是固定的(选取最小标号叶子节点),所以每棵无根树只对应唯一一个prufer数列
    由于prufer数列在还原成树时每步操作也是唯一的,所以每个prufer数列只能还原成唯一的一棵树


既然prufer数列和有编号无根树一一对应,那么我们就可以把有编号无根树转为prufer数列计数

假如我们选出的点度数分别为 a1,a2,...,as a 1 , a 2 , . . . , a s ,那么每个点的出现次数就分别为 a11,a21,...,as1 a 1 − 1 , a 2 − 1 , . . . , a s − 1
得到一个 s2 s − 2 的序列,我们可以把这 s2 s − 2 个数任意排列,不同的排列就对应着该情况下的一种合法树,即:

(s2)!si=1(ai1)! ( s − 2 ) ! ∏ i = 1 s ( a i − 1 ) !

注意:上面讨论的都是确定结点度数的情况

设计状态
f[i][j][k] f [ i ] [ j ] [ k ] 表示前 i i 个点,选了j个点,当前的 prufer p r u f e r 数列长度为 k k 时的方案数

如果当前这个点不选:f[i][j][k]=f[i1][j][k]
如果选择当前点,枚举这个点在序列中出现的次数(度数-1): f[i][j][k]=ai1d=0f[i1][j1][kd](k>=d) f [ i ] [ j ] [ k ] = ∑ d = 0 a i − 1 f [ i − 1 ] [ j − 1 ] [ k − d ] ( k >= d )

知道了每个数在prufer数列中出现的次数,那么不同的prufer数列个数就好求了
cnti c n t i 为第 i i 个数字在prufer数列中出现的次数,p为当前选择的元素个数

ans=f[n][p][p2](p2)!ni=1cnti! a n s = f [ n ] [ p ] [ p − 2 ] ∗ ( p − 2 ) ! ∏ i = 1 n c n t i !

然而我们在转移的时候,并没有记录 cnt c n t
所以我们不要在最后处理,直接在状态转移的时候维护上式

f[i][j][k]=d=0ai1f[i1][j1][kd]d!(k>=d) f [ i ] [ j ] [ k ] = ∑ d = 0 a i − 1 f [ i − 1 ] [ j − 1 ] [ k − d ] d ! ( k >= d )

因此,最后答案: f[n][p][p2](p2)! f [ n ] [ p ] [ p − 2 ] ∗ ( p − 2 ) !

时间复杂度: O(n4) O ( n 4 )
(因为不合法状态比较多,对于 n<=100 n <= 100 的数据, n4 n 4 的复杂度还是可以过的)

tip

注意代码中转移的写法
i,j,k i , j , k 的上界都是n。。。我一开始 j>i,k>j j − > i , k − > j 就狂WA不止

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long

using namespace std;

const ll p=19260817;
const int N=105;
int n,a[N];
ll f[N][N][N],jc[N],inv[N];

ll KSM(ll a,ll b) {
    ll t=1;
    while (b) {
        if (b&1) t=(t*a)%p;
        b>>=1;
        a=(a*a)%p;
    }
    return t%p;
}

void solve()
{
    f[0][0][0]=1; 
    for (int i=0;i<n;i++)
        for (int j=0;j<n;j++) 
            for (int k=0;k<n;k++) if (f[i][j][k]) {
                f[i+1][j][k]=(f[i+1][j][k]+f[i][j][k])%p;
                for (int d=0;d<a[i+1]&&d+k<=n-2;d++) 
                    f[i+1][j+1][k+d]=(f[i+1][j+1][k+d]+f[i][j][k]*inv[d]%p)%p;
            }
}

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

    jc[0]=1;
    for (int i=1;i<=100;i++) jc[i]=jc[i-1]*(ll)i%p;
    for (int i=0;i<=100;i++) inv[i]=KSM(jc[i],p-2)%p;

    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    solve();

    printf("%d ",n);
    for (int i=2;i<=n;i++) {
        ll ans=f[n][i][i-2]*jc[i-2]%p;
        printf("%lld ",ans);
    } 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值