【JZOJ5068】【GDSOI2017第二轮模拟】树 动态规划+prufer序列

题面

有n个点,它们从1到n进行标号,第i个点的限制为度数不能超过A[i].
现在对于每个s (1 <= s <= n),问从这n个点中选出一些点组成大小为s的有标号无根树的方案数。
100%的数据:n <= 100

100

prufer序列

每个大小为n,有标号无根树都可以表示成一个长度为(n-2)且取值在[1,n]的序列。

这个序列就叫prufer序列

树转prufer序列

1.每次查找一个标号最小且度数为一的点,使与之相连的点的编号加入序列尾;
2.删除树中的这个点。

prufer序列转树

1.新建一个包含[1,n]的集合;
2.找出集合中在序列中未出现的最小编号,并使之与序列头连一条边;
3.删除集合中的这个编号,以及序列头;
4.重复2-3步骤n-2次,序列则为空;
5.把集合中的剩余两个编号相连,原树复原。

性质

1.长度为(n-2)的prufer序列的集合与大小为n的带标号无根树的集合一一映射。
2.带标号无根树的任意点的度数等于,与之对应的prufer序列的对应编号出现次数+1。


Back to Problem

Pre

假设我们求出了一个prufer序列的长度\(n\),并且知道其中每个编号出现的次数\(c_i\)
由性质2,那么这种prufer序列的贡献就是,它的不同全排列数,有:
\[Ans=n!*\sum_{i=1}^n\frac{1}{c_i!}\]

正文

所以我们把原题转化为在序列上的动态规划
\(f_{i,j,k}\)表示前i个点,用了j个点,序列长度为k的方案数。
那么就有,
\[ f_{i,j,k} \Rightarrow \begin{cases} f_{i+1,j,k}\\ f_{i+1,j+1,k+l},&l \in [0,a_i-1] \end{cases} \]
最终答案就是\(f_{n,s,s-2}*(s-2)!\)
时间复杂度为\(O(n^4)\),但由于冗余状态的存在,是能卡过的。
原题是要用FFT优化到\(O(n^3*log_n)\)

Code

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define ll long long
#define fo(i,x,y) for(int i=x;i<=y;i++)
#define fd(i,x,y) for(int i=x;i>=y;i--)
using namespace std;
const char* fin="tree.in";
const char* fout="tree.out";
const int inf=0x7fffffff;
const int maxn=107,mo=1004535809;
int n,a[maxn],f[maxn][maxn][maxn],fact[maxn],nf[maxn];
int qpower(int a,int b){
    int c=1;
    while (b){
        if (b&1) c=1LL*a*c%mo;
        a=1LL*a*a%mo;
        b>>=1;
    }
    return c;
}
int ni(int v){return qpower(v,mo-2);}
int main(){
    freopen(fin,"r",stdin);
    freopen(fout,"w",stdout);
    scanf("%d",&n);
    fact[0]=1;
    fo(i,1,maxn-1) fact[i]=1LL*fact[i-1]*i%mo;
    fo(i,0,maxn-1) nf[i]=ni(fact[i]);
    fo(i,1,n) scanf("%d",&a[i]);
    memset(f,0,sizeof f);
    f[1][0][0]=1;
    fo(i,1,n)
        fo(j,0,i-1)
            fo(k,0,n){
                f[i+1][j][k]=(f[i][j][k]+f[i+1][j][k])%mo;
                fo(l,0,a[i]-1){
                    if (k+l>n) break;
                    f[i+1][j+1][k+l]=(f[i+1][j+1][k+l]+1LL*f[i][j][k]*nf[l])%mo;
                }
            }
    printf("%d ",n);
    fo(i,2,n) cout<<1LL*f[n+1][i][i-2]*fact[i-2]%mo<<" ";
    return 0;
}

转载于:https://www.cnblogs.com/hiweibolu/p/6725236.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值