LibreOJ #2320.「清华集训 2017」生成树计数 prufer序列+组合数学+分治FFT

该博客讨论了如何利用Prufer序列和组合数学解决生成树计数问题,特别是在处理点权和度数时的策略。文章通过分析展开求和公式,提出了一种暴力解决方案,并进一步优化,将问题转化为颜色涂格子的问题。最后,博主给出了问题的代码实现,探讨了如何处理每个点出现次数的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题意

nn个点,每个点有点权ai,定义一棵生成树TT中第i个点的度数为didi,那么该生成树的权值

val(T)=(i=1nadiidmi)(i=1ndmi)val(T)=(∏i=1naididim)(∑i=1ndim)

求所有生成树的价值的和,答案模998244353。
n30000,m30n≤30000,m≤30

分析

我们把右边的展开后可以看成,生成树其中一个点度数的指数是2m2m其余都是mm
先考虑暴力,我们设fi,j表示只考虑前面那项,满足前ii个点在prufer中选了j个位置的所有生成树的权值和,gi,jgi,j则表示前ii个点且恰好有一个点的指数是2m的权值和。
转移比较显然,考虑如何优化。
注意到mm非常小,那么我们可以把dim看成是用mm种颜色去涂di个格子,每个格子可以涂任意数量的颜色的方案数。
因为在prufer序列中每个点出现的次数是di1di−1,所以我们在后面加上1n1到n,这样每个点出现的次数就是didi了。
这样的话我们就可以枚举每个点涂了哪些格子,然后剩下没涂的格子随便填就好了。
fi,jfi,jgi,jgi,j的定义不变,只是改成了前ii个点填了j个格子的方案。
转移式推出来大概长这样:

fi,j=k=0mfi1,jkCkn2j+kak+1i(S(m,k)k!+S(m,k+1)(k+1)!)fi,j=∑k=0mfi−1,j−kCn−2−j+kkaik+1(S(m,k)k!+S(m,k+1)(k+1)!)

gi,j=s1+s2gi,j=s1+s2

s1=k=0mgi1,jkCkn2j+kak+1i(S(m,k)k!+S(m,k+1)(k+1)!)s1=∑k=0mgi−1,j−kCn−2−j+kkaik+1(S(m,k)k!+S(m,k+1)(k+1)!)

s2=k=02mfi1,jkCkn2j+kak+1i(S(2m,k)k!+S(2m,k+1)(k+1)!)s2=∑k=02mfi−1,j−kCn−2−j+kkaik+1(S(2m,k)k!+S(2m,k+1)(k+1)!)

化简一下可以变成
Fi=Fi1AiFi=Fi−1∗Ai

Gi=Gi1Ai+Fi1BiGi=Gi−1∗Ai+Fi−1∗Bi

其中上面每个变量都是多项式,那么用分治FFT优化下就好了。

代码

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

typedef long long LL;

const int N=30005;
const int M=65;
const int MOD=998244353;

int n,m,jc[N],ny[N],a[N],s[M][M],d1[N][M],d2[N][M],L,rev[N*M*2],f[20][N*M*2],g[20][N*M*2];

int ksm(int x,int y)
{
    int ans=1;
    while (y)
    {
        if (y&1) ans=(LL)ans*x%MOD;
        x=(LL)x*x%MOD;y>>=1;
    }
    return ans;
}

void NTT(int *a,int f)
{
    for (int i=0;i<L;i++) if (i<rev[i]) std::swap(a[i],a[rev[i]]);
    for (int i=1;i<L;i<<=1)
    {
        int wn=ksm(3,f==1?(MOD-1)/i/2:MOD-1-(MOD-1)/i/2);
        for (int j=0;j<L;j+=(i<<1))
        {
            int w=1;
            for (int k=0;k<i;k++)
            {
                int u=a[j+k],v=(LL)a[j+k+i]*w%MOD;
                a[j+k]=(u+v)%MOD;a[j+k+i]=(u+MOD-v)%MOD;
                w=(LL)w*wn%MOD;
            }
        }
    }
    int ny=ksm(L,MOD-2);
    if (f==-1) for (int i=0;i<L;i++) a[i]=(LL)a[i]*ny%MOD;
}

void pre()
{
    s[1][1]=1;
    for (int i=2;i<=m*2;i++)
        for (int j=1;j<=i;j++)
            s[i][j]=(s[i-1][j-1]+(LL)s[i-1][j]*j%MOD)%MOD;
    for (int i=1;i<=n;i++)
    {
        d1[i][0]=d2[i][0]=1;
        for (int j=1;j<=m;j++) d1[i][j]=(LL)d1[i][j-1]*a[i]%MOD;
        for (int j=1;j<=m*2;j++) d2[i][j]=(LL)d2[i][j-1]*a[i]%MOD;
    }
    for (int i=1;i<=n;i++)
    {
        for (int j=0;j<=m;j++) d1[i][j]=(LL)d1[i][j]*ny[j]%MOD*((LL)s[m][j]*jc[j]%MOD+(LL)s[m][j+1]*jc[j+1]%MOD)%MOD;
        for (int j=0;j<=m*2;j++) d2[i][j]=(LL)d2[i][j]*ny[j]%MOD*((LL)s[m*2][j]*jc[j]%MOD+(LL)s[m*2][j+1]*jc[j+1]%MOD)%MOD;
    }
}

void solve(int l,int r,int d)
{
    if (l==r)
    {
        for (int i=0;i<=m*2;i++) f[d][i]=d1[l][i],g[d][i]=d2[l][i];
        return;
    }
    int mid=(l+r)/2,L1=m*(mid-l+1)+m,L2=m*(r-mid)+m;
    solve(l,mid,d+1);
    for (int i=0;i<=L1;i++) f[d][i]=f[d+1][i],g[d][i]=g[d+1][i];
    solve(mid+1,r,d+1);
    int lg=0;
    for (L=1;L<=L1+L2;L<<=1,lg++);
    for (int i=0;i<L;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<(lg-1));
    for (int i=L1+1;i<L;i++) f[d][i]=g[d][i]=0;
    for (int i=L2+1;i<L;i++) f[d+1][i]=g[d+1][i]=0;
    NTT(f[d],1);NTT(g[d],1);NTT(f[d+1],1);NTT(g[d+1],1);
    for (int i=0;i<L;i++) g[d][i]=((LL)f[d][i]*g[d+1][i]+(LL)g[d][i]*f[d+1][i])%MOD,f[d][i]=(LL)f[d][i]*f[d+1][i]%MOD;
    NTT(f[d],-1);NTT(g[d],-1);
}

int main()
{
    scanf("%d%d",&n,&m);
    jc[0]=jc[1]=ny[0]=ny[1]=1;
    for (int i=2;i<=n;i++) jc[i]=(LL)jc[i-1]*i%MOD,ny[i]=(LL)(MOD-MOD/i)*ny[MOD%i]%MOD;
    for (int i=2;i<=n;i++) ny[i]=(LL)ny[i-1]*ny[i]%MOD;
    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    pre();
    solve(1,n,0);
    int sum=0;
    for (int i=1;i<=n;i++) (sum+=a[i])%=MOD;
    int ans=0;
    for (int i=0;i<=n-2;i++)
        (ans+=(LL)g[0][i]*ksm(sum,n-2-i)%MOD*ny[n-2-i]%MOD)%=MOD;
    for (int i=1;i<=n;i++) ans=(LL)ans*a[i]%MOD;
    printf("%d",(LL)ans*jc[n-2]%MOD);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值