2022牛客#2 E 字符串由多少特定子串的计数,容斥,二项式反演,ntt加速

这篇博客介绍了如何使用容斥原理和组合计数来解决计算恰好有k个特定子串的字符串数量的问题。提出了一个递推公式,并通过非负定值卷积的NTT(快速傅里叶变换)算法进行高效计算。文章详细阐述了NTT的过程和实现细节,包括二项式反演、系数分离以及卷积操作,最后给出C++代码示例来演示计算过程。

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

题意:

f(n,k)f(n,k)f(n,k)为有kkkbitbitbit子串的仅有小写字母构成的长度为nnn字符串的种数。给定一个整数nnn,求f(n,0),f(n,1),..,f(n,n)f(n,0),f(n,1),..,f(n,n)f(n,0)f(n,1)..f(n,n)

方法:

(容斥原理,组合计数)

f(k)f(k)f(k)为恰好有kkk个子串bitbitbit的数量,要求他,组合数学的角度显然得令其中kkk个位bitbitbit,我们把三位缩成一位考虑,那么就是在n−2kn-2kn2k位内挑选bbb字符的位置,即Cn−2kkC_{n-2k}^{k}Cn2kk,其他字符不好确定没有bitbitbit的情况,因此利用容斥,先计算至少含有kkkbitbitbit的数量,那么就是挑选的了kkkbbb字符的位置,然后剩下的位置任意放,即Cn−2kk26n−3kC_{n-2k}^{k}26^{n-3k}Cn2kk26n3k

但是我们这种其实是钦定了kkkbitbitbit,其他位置仍然可能有bitbitbit,并且会对恰好有k+ik+ik+ibitbitbit的数量重复计算了Ck+ikC_{k+i}^{k}Ck+ik次。

证明:任选一个恰好k+ik+ik+ibitbitbit的方案,任意选择kkkbitbitbit作为我们钦定的,实际上钦定方式没有差别,所以重复计算了有多少种kkkbitbitbit可以选择,即Ck+ikC_{k+i}^{k}Ck+ik。我们知道了那些方案重复计算了多少次,那么我们就可以写出递推式:f(k)=Cn−2kk26n−3k−∑i=k+1n/3Cikf(i)f(k)=C_{n-2k}^{k}26^{n-3k}-\sum_{i=k+1}^{n/3}C_{i}^{k}f(i)f(k)=Cn2kk26n3ki=k+1n/3Cikf(i)

把函数写在一边,得到∑i=kn/3Cikf(i)=Cn−2kk26n−3k\sum_{i=k}^{n/3}C_{i}^{k}f(i)=C_{n-2k}^{k}26^{n-3k}i=kn/3Cikf(i)=Cn2kk26n3k

由二项式反演:f(n)=∑i=nmCing(i)  ⟺  g(n)=∑i=nmCin(−1)i−nf(i)f(n)=\sum_{i=n}^{m}C_{i}^{n}g(i) \iff g(n)=\sum_{i=n}^{m}C_{i}^{n}(-1)^{i-n}f(i)f(n)=i=nmCing(i)g(n)=i=nmCin(1)inf(i)

得到:f(k)=∑i=kn/3(−1)i−kCikCn−2ii26n−3if(k)=\sum_{i=k}^{n/3}(-1)^{i-k}C_{i}^{k}C_{n-2i}^{i}26^{n-3i}f(k)=i=kn/3(1)ikCikCn2ii26n3i

展开得到:

f(k)=∑i=kn/3(−1)i−k(n−2i)!26n−3ik!(n−3i)!(i−k)!f(k)=\sum_{i=k}^{n/3}(-1)^{i-k}\frac{(n-2i)!26^{n-3i}}{k!(n-3i)!(i-k)!}f(k)=i=kn/3(1)ikk!(n3i)!(ik)!(n2i)!26n3i

先把求和内的系数分离,得到

f(k)=(−1)kk!∑i=kn/3(−1)i(−1)i(n−2i)!26n−3i(n−3i)!(i−k)!f(k)=\frac{(-1)^{k}}{k!}\sum_{i=k}^{n/3}(-1)^{i}\frac{(-1)^{i}(n-2i)!26^{n-3i}}{(n-3i)!(i-k)!}f(k)=k!(1)ki=kn/3(1)i(n3i)!(ik)!(1)i(n2i)!26n3i

剩下的计算繁琐,但是注意到存在−i-ii+i+i+i项,我们考虑凑定值卷积,并且我们凑的这个定值要是非负的

考虑令Pi=1(−i)!,Qi=(−1)i(n−2i)!26n−3i(n−3i)P_{i}=\frac{1}{(-i)!},Q_{i}=\frac{(-1)^{i}(n-2i)!26^{n-3i}}{(n-3i)}Pi=(i)!1,Qi=(n3i)(1)i(n2i)!26n3i

那么原式=(−1)kk!∑i=kn/3Pk−iQi=\frac{(-1)^{k}}{k!}\sum_{i=k}^{n/3}P_{k-i}Q_{i}=k!(1)ki=kn/3PkiQi,但由于k−ik-iki是负数,数组存不了,我们考虑将PPP平移一下:

重新令Pi=1(n3−i)!P_{i}=\frac{1}{(\frac{n}{3}-i)!}Pi=(3ni)!1QQQ不变

那么原式=(−1)kk!∑i=kn/3Pn3+k−iQi=\frac{(-1)^{k}}{k!}\sum_{i=k}^{n/3}P_{\frac{n}{3}+k-i}Q_{i}=k!(1)ki=kn/3P3n+kiQi,可以利用卷积

tips:

原式的求和就是所有P,QP,QP,Q卷一起的第n3+k\frac{n}{3}+k3n+k项,不要误以为是挑几项出来乘积

目前遇到的nttnttntt模型:

1.和为定值卷积,卷积结果第iii项就是∑j=0iajbi−j\sum_{j=0}^{i}a_{j}b_{i-j}j=0iajbij

2.出现了定值卷积但是是前面的推后面的递推形式,cdqcdqcdq分治nttnttntt,并且在分治到l==rl==rl==r时递推

#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;

const long long mod=998244353,inv3=332748118;

ll qpow(ll a,ll b)
{
    ll ret=1,base=a;
    while(b)
    {
        if(b&1) ret=ret*base%mod;
        base=base*base%mod;
        b>>=1;
    }
    return ret;
}

int getlen(int k)
{
    int ret=0;
    while(k){k>>=1;ret++;}
    return ret;
}

int getrev(int k,int len)
{
    int ret=0;
    while(k){ret=ret<<1|(k&1);len--;k>>=1;}
    while(len--) ret<<=1;
    return ret;
}

vector<int>pos(2100000);

inline void ntt(vector<ll>&a,int limit,int op)
{
    for(int i=0;i<limit;i++)
        if(i<pos[i]) swap(a[i],a[pos[i]]);
    for(int len=2;len<=limit;len<<=1)
    {
        ll base=qpow(op==1?3ll:inv3,(mod-1)/len);
        for(int l=0;l<limit;l+=len)
        {
            ll now=1;
            for(int i=l;i<l+len/2;i++)
            {
                ll temp1=a[i],temp2=now*a[i+len/2]%mod;
                a[i]=(temp1+temp2)%mod;
                a[i+len/2]=(temp1-temp2+mod)%mod;
                now=now*base%mod;
            }
        }
    }
}

int n;
vector<ll>a(2100000),b(2100000);
ll fac[1000001],facinv[1000001],p[1000005];

inline void multi(int limit)
{
    int len=getlen(limit-1);
    for(int i=0;i<limit;i++) pos[i]=getrev(i,len);
    ntt(a,limit,1);ntt(b,limit,1);
    for(int i=0;i<limit;i++) a[i]=(a[i]*b[i])%mod;
    ntt(a,limit,-1);
    ll temp=qpow(limit,mod-2);
    for(int i=0;i<limit;i++) a[i]=a[i]*temp%mod;
}

inline ll P(int x){return facinv[x];}

inline ll Q(int x){return (x&1?-1:1)*fac[n-2*x]*p[n-3*x]%mod*facinv[n-3*x]%mod;} 

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);cout.tie(nullptr);
    cin>>n;fac[0]=p[0]=facinv[0]=1;
    for(int i=1;i<=n;i++)
    {
        fac[i]=fac[i-1]*i%mod;
        facinv[i]=qpow(fac[i],mod-2);
        p[i]=p[i-1]*26%mod;
    }
    for(int i=0;i<=n/3;i++) a[i]=P(n/3-i),b[i]=Q(i);
    // for(int i=1;i<=n;i++) printf("a[%d]=%lld\n",i,a[i]);
    int limit=1;
    while(limit<=2*n/3+2) limit<<=1;
    multi(limit);
    for(int i=0;i<=n/3;i++) cout<<(((i&1?-1:1)*a[i+n/3]*facinv[i])%mod+mod)%mod<<" ";
    for(int i=n/3+1;i<=n;i++) cout<<0<<" ";
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值