NTT(FFT)+CDQ优化DP

DP柿子满足卷积形式均可以用CDQ+NTT优化

\small dp[i]=\sum_{k=1}^{i}dp[i-k]*f1[k]/f2[i-k] =\sum ai*bi,ai,bi,为两个生成函数的系数

 

例题1.hdu5322  模数资瓷NTT,ans=dp[n],n<=1e5(下面柿子的(j-1)!应该是(i-1)! )

系数ai=dp[i]/i! ,bi=i*i,在CDQ里用NTT

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<bits/stdc++.h>
#define ll long long
using namespace std;
//AC hdu5322 998ms
inline int gi(){//快读 
    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<<3)+(x<<1)+ch-'0';ch=getchar();}
    return f?x:-x;
}

const int _ = 4e5+5;//2倍 
const int mod = 998244353;//模数 
ll n,m,s,N,lim,ans;ll jc[_];
ll inv[_],a[_],b[_],rev[_],l,og[_];//NTT本身用到的数组 
ll qmod(ll a,ll b){//快速幂 
    int res=1;
    while (b) {if (b&1) res=1ll*res*a%mod;a=1ll*a*a%mod;b>>=1;}
    return res;
}
void ntt(ll *P,ll len,int opt){//基本上不用改 
    for (int i=0;i<len;++i) if (i<rev[i]) swap(P[i],P[rev[i]]);
    for (int i=1;i<len;i<<=1){
        ll W=qmod(3,(mod-1)/(i<<1));//3 根据模数的原根  可能要改 
        if (opt==-1) W=qmod(W,mod-2);
        og[0]=1;
        for (int j=1;j<i;++j) og[j]=1ll*og[j-1]*W%mod;
        for (int p=i<<1,j=0;j<len;j+=p)
            for (int k=0;k<i;++k){
                ll x=P[j+k],y=1ll*og[k]*P[j+k+i]%mod;
                P[j+k]=(x+y)%mod,P[j+k+i]=(x-y+mod)%mod;
            }
    }
    if (opt==-1) for (int i=0,Inv=qmod(len,mod-2);i<len;++i) P[i]=1ll*P[i]*Inv%mod;
}

ll dp[100005];
void cdq(int l,int r)
{
	if(l==r)return ;
	int m=l+r>>1;
	cdq(l,m);
	ll len,lll=0;for (len=1;len<=((r-l+1)<<1);len<<=1) ++lll;--lll;//固定 
	for (int i=0;i<len;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<lll);//固定 
	
	//预处理系数 
	for(int i=0;i<len;i++){
		a[i]=b[i]=0;
		if(i<=r-l+1) b[i]=1ll*i*i%mod;//预处理bi 不固定  非dp项1~r-l+1 
	}
	for(int i=l;i<=m;i++) a[i-l+1]=1ll*dp[i]*inv[i]%mod;//预处理ai 不固定 dp项 l~m
	
	ntt(a,len,1);ntt(b,len,1); 
    for (int i=0;i<len;++i) a[i]=1ll*a[i]*b[i]%mod;
    ntt(a,len,-1);
    
    for(int i=m+1;i<=r;i++) dp[i]=(dp[i]+1ll*a[i-l+1]*jc[i-1]%mod)%mod;//将[l~m]的影响加到dp[m+1~r]上 
    cdq(m+1,r);
}


int main(){
    //n=gi();m=gi();//快读
	//N=max(n,m);//求和表达式 上限   (3.会用到) 
	lim=_-3;//阶乘上限   (非NTT)
	
	//预处理阶乘 跟逆元阶乘 (非NTT)
    jc[0]=1; for (int i=1;i<=lim;++i) jc[i]=1ll*jc[i-1]*i%mod;
    inv[lim]=qmod(jc[lim],mod-2);
    for (int i=lim;i;--i) inv[i-1]=1ll*inv[i]*i%mod;
    
    memset(dp,0,sizeof(dp));
	dp[0]=1;
    cdq(0,100000);//预处理DP 
    while(~scanf("%d",&n))printf("%d\n",dp[n]%mod);
    
    
    //1.NTT   固定操作,处理数据的两倍 对2^k向上取整=len 
    //for (len=1;len<=(N<<1);len<<=1) ++l;--l;
    
    //2.NTT	  固定操作,预处理rev 蝶形变换 
    //for (int i=0;i<len;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<l);
    
    //3.NTT   灵活操作  ai bi的表达式 
    //for (int i=0;i<=n;++i) a[i]=gi();
    //for (int i=0;i<=m;++i) b[i]=gi();
    
    //4.NTT   固定操作  ai卷积bi  也就是(a0+a1x+a2x^2...)*(b0+b1x+b2x^2....)=sum_{i=0...n}sum_{j=0...i}ai*b(j-i)
    //ntt(a,1);ntt(b,1); 
    //for (int i=0;i<len;++i) a[i]=1ll*a[i]*b[i]%mod;
    //ntt(a,-1);
    
    //5.NTT非固定操作  计算求和表达式的答案ans  
    //for (int i=0;i<=n+m;++i) printf("%d ",a[i]); puts("");
    //for(int i=0;i<=m;i++) printf("%d ",a[i]);
    //printf("%d\n",ans);
    
    
    return 0;
}

 

例题2.hdu5730   ans=dp[n],n<=1e5

ai=dp[i] bi=a[i],a[i]是题目给的= =

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
const double eps=1e-8;

const double PI = acos(-1.0);//精度不够会WA换long double 

struct Complex{
    double real, image;//精度不够会WA换long double 
    Complex(double _real, double _image)//精度不够会WA换long double 
    {
        real = _real;
        image = _image;
    }
    Complex() {}
};
Complex operator + (const Complex &c1, const Complex &c2){
    return Complex(c1.real + c2.real, c1.image + c2.image);
}
Complex operator - (const Complex &c1, const Complex &c2){
    return Complex(c1.real - c2.real, c1.image - c2.image);
}
Complex operator * (const Complex &c1, const Complex &c2){
    return Complex(c1.real*c2.real - c1.image*c2.image, c1.real*c2.image + c1.image*c2.real);
}
int rev(int id, int len)
{
    int ret = 0;
    for(int i = 0; (1 << i) < len; i++)
    {
        ret <<= 1;
        if(id & (1 << i)) ret |= 1;
    }
    return ret;
}
Complex A[1 << 19];//tmp
void FFT(Complex *a, int len, int DFT)
{
    for(int i = 0; i < len; i++)
        A[rev(i, len)] = a[i];
    for(int s = 1; (1 << s) <= len; s++)
    {
        int m = (1 << s);
        Complex wm = Complex(cos(DFT*2*PI/m), sin(DFT*2*PI/m));
        for(int k = 0; k < len; k += m)
        {
            Complex w = Complex(1, 0);
            for(int j = 0; j < (m >> 1); j++)
            {
                Complex t = w*A[k + j + (m >> 1)];
                Complex u = A[k + j];
                A[k + j] = u + t;
                A[k + j + (m >> 1)] = u - t;
                w = w*wm;
            }
        }
    }
    if(DFT == -1) for(int i = 0; i < len; i++) A[i].real /= len, A[i].image /= len;
    for(int i = 0; i < len; i++) a[i] = A[i];
    return;
}
Complex a[1<<19];//系数数组,后面要移到复数数组,好像直接开复数数组比较好, 
Complex b[1<<19];
int dp[100005];int vv[100005];
void cdq(int l,int r)
{
	if(l==r) return ;
	int m=l+r>>1;
	cdq(l,m);
	int len=1;while(len<=r-l+1)len=len<<1;len=len<<1;
	
	//预处理系数 
	for(int i=0;i<len;i++)a[i]=b[i]=Complex(0,0);
	for(int i=l;i<=m;i++)b[i-l+1]=Complex(dp[i],0);//dp 只能l~m 
	for(int i=1;i<=r-l+1;i++)a[i]=Complex(vv[i],0);//非dp项需要l~r 
	
	FFT(a,len,1); FFT(b,len,1);
	for(int i=0;i<len;i++)a[i]=a[i]*b[i];
	FFT(a,len,-1);
	
	for(int i=m+1;i<=r;i++)dp[i]=dp[i]+(ll)(a[i-l+1].real+0.5),dp[i]%=313;
	cdq(m+1,r);
}
int main()
{
   int n;
   while(~scanf("%d",&n))
   {
   		if(n==0)break;
   		memset(vv,0,sizeof(vv));
   		for(int i=1;i<=n;i++)scanf("%d",&vv[i]),vv[i]%=313;
   		memset(dp,0,sizeof(dp));
   		dp[0]=1;
   		cdq(0,n);
   		printf("%d\n",dp[n]%313);
   }
   
}

 

例题3.BZOJ4555   n<=1e5

\small DP[n]=n!\sum_{i=1}^{n}\frac{2*DP[n-i]}{i!*(n-i)!},ans=\sum_{i=0}^{n}DP[i]

直接上 bi=2/i!,ai=dpi/i!

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<bits/stdc++.h>
#define ll long long
using namespace std;
//AC hdu5322 998ms
//AC BZOJ4555 26111ms
//dp[i]=i!*sum dp[i-j]*2/j!;
inline int gi(){//快读 
    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<<3)+(x<<1)+ch-'0';ch=getchar();}
    return f?x:-x;
}

const int _ = 4e5+5;//2倍 
const int mod = 998244353;//模数 
int n,m,s,N,lim,ans;int jc[_];
int inv[_],a[_],b[_],rev[_],l,og[_];//NTT本身用到的数组 
int qmod(int a,int b){//快速幂 
    int res=1;
    while (b) {if (b&1) res=1ll*res*a%mod;a=1ll*a*a%mod;b>>=1;}
    return res;
}
void ntt(int *P,int len,int opt){//基本上不用改 
    for (int i=0;i<len;++i) if (i<rev[i]) swap(P[i],P[rev[i]]);
    for (int i=1;i<len;i<<=1){
        ll W=qmod(3,(mod-1)/(i<<1));//3 根据模数的原根  可能要改 
        if (opt==-1) W=qmod(W,mod-2);
        og[0]=1;
        for (int j=1;j<i;++j) og[j]=1ll*og[j-1]*W%mod;
        for (int p=i<<1,j=0;j<len;j+=p)
            for (int k=0;k<i;++k){
                ll x=P[j+k],y=1ll*og[k]*P[j+k+i]%mod;
                P[j+k]=(x+y)%mod,P[j+k+i]=(x-y+mod)%mod;
            }
    }
    if (opt==-1) for (int i=0,Inv=qmod(len,mod-2);i<len;++i) P[i]=1ll*P[i]*Inv%mod;
}

ll dp[100005];
void cdq(int l,int r)
{
	if(l==r)return ;
	int m=l+r>>1;
	cdq(l,m);
	ll len,lll=0;for (len=1;len<=((r-l+1)<<1);len<<=1) ++lll;--lll;//固定 
	for (int i=0;i<len;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<lll);//固定 
	//预处理系数 
	for(int i=0;i<len;i++){
		a[i]=b[i]=0;
		if(i<=r-l+1) b[i]=1ll*2*inv[i]%mod;//预处理bi 不固定  非dp项1~r-l+1 
	}
	for(int i=l;i<=m;i++) a[i-l+1]=1ll*dp[i]*inv[i]%mod;//预处理ai 不固定 dp项 l~m
	ntt(a,len,1);ntt(b,len,1); 
    for (int i=0;i<len;++i) a[i]=1ll*a[i]*b[i]%mod;
    ntt(a,len,-1);
    for(int i=m+1;i<=r;i++) (dp[i]+=1ll*a[i-l+1]*jc[i]%mod)%=mod;//将[l~m]的影响加到dp[m+1~r]上 
    cdq(m+1,r);
}


int main(){
    //n=gi();m=gi();//快读
	//N=max(n,m);//求和表达式 上限   (3.会用到) 
	lim=_-3;//阶乘上限   (非NTT)
	
	//预处理阶乘 跟逆元阶乘 (非NTT)
    jc[0]=1; for (int i=1;i<=lim;++i) jc[i]=1ll*jc[i-1]*i%mod;
    inv[lim]=qmod(jc[lim],mod-2);
    for (int i=lim;i>=1;--i) inv[i-1]=1ll*inv[i]*i%mod;inv[0]=1;
    
    //memset(dp,0,sizeof(dp));
	dp[0]=1;
	scanf("%d",&n);
    cdq(0,n);//预处理DP 
    
	int ans=0;
	for(int i=0;i<=n;i++)(ans+=1ll*dp[i])%=mod;
	printf("%d\n",ans%mod);
    
    
    //1.NTT   固定操作,处理数据的两倍 对2^k向上取整=len 
    //for (len=1;len<=(N<<1);len<<=1) ++l;--l;
    
    //2.NTT	  固定操作,预处理rev 蝶形变换 
    //for (int i=0;i<len;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<l);
    
    //3.NTT   灵活操作  ai bi的表达式 
    //for (int i=0;i<=n;++i) a[i]=gi();
    //for (int i=0;i<=m;++i) b[i]=gi();
    
    //4.NTT   固定操作  ai卷积bi  也就是(a0+a1x+a2x^2...)*(b0+b1x+b2x^2....)=sum_{i=0...n}sum_{j=0...i}ai*b(j-i)
    //ntt(a,1);ntt(b,1); 
    //for (int i=0;i<len;++i) a[i]=1ll*a[i]*b[i]%mod;
    //ntt(a,-1);
    
    //5.NTT非固定操作  计算求和表达式的答案ans  
    //for (int i=0;i<=n+m;++i) printf("%d ",a[i]); puts("");
    //for(int i=0;i<=m;i++) printf("%d ",a[i]);
    //printf("%d\n",ans);
    
    
    return 0;
}

 

例题4:51nod1514  NTT+DP

题意:dp[n]表示:满足任意1<=i<=n-1,下标1~i都是1~i的排列的1~n的排列

\large dp[n]=n!-\sum_{i=0}^{n-1}dp[i]*(n-i)!

由于前面有n!,所以CDQ里l==r加上n!对DP的贡献

#include<bits/stdc++.h>
#define ll long long
using namespace std;
//NTT+CDQ
//AC hdu5322 998ms
//AC BZOJ4555 26111ms
//dp[i]=i!*sum dp[i-j]*2/j!;
inline int gi(){//快读 
    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<<3)+(x<<1)+ch-'0';ch=getchar();}
    return f?x:-x;
}

const int _ = 4e5+5;//2倍 
const int mod = 998244353;//模数 
int n,m,s,N,lim,ans;int jc[_];
int inv[_],a[_],b[_],rev[_],l,og[_];//NTT本身用到的数组 
int qmod(int a,int b){//快速幂 
    int res=1;
    while (b) {if (b&1) res=1ll*res*a%mod;a=1ll*a*a%mod;b>>=1;}
    return res;
}
void ntt(int *P,int len,int opt){//基本上不用改 
    for (int i=0;i<len;++i) if (i<rev[i]) swap(P[i],P[rev[i]]);
    for (int i=1;i<len;i<<=1){
        ll W=qmod(3,(mod-1)/(i<<1));//3 根据模数的原根  可能要改 
        if (opt==-1) W=qmod(W,mod-2);
        og[0]=1;
        for (int j=1;j<i;++j) og[j]=1ll*og[j-1]*W%mod;
        for (int p=i<<1,j=0;j<len;j+=p)
            for (int k=0;k<i;++k){
                ll x=P[j+k],y=1ll*og[k]*P[j+k+i]%mod;
                P[j+k]=(x+y)%mod,P[j+k+i]=(x-y+mod)%mod;
            }
    }
    if (opt==-1) for (int i=0,Inv=qmod(len,mod-2);i<len;++i) P[i]=1ll*P[i]*Inv%mod;
}

ll dp[100005];
void cdq(int l,int r)
{
	if(l==r)return (void)(dp[l]=(jc[l]-dp[l]+mod)%mod);
	int m=l+r>>1;
	cdq(l,m);
	ll len,lll=0;for (len=1;len<=((r-l+1)<<1);len<<=1) ++lll;--lll;//固定 
	for (int i=0;i<len;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<lll);//固定 
	
	//预处理系数 
	for(int i=0;i<len;i++){
		a[i]=b[i]=0;
		if(i<=r-l+1) b[i]=1ll*jc[i]%mod;//预处理bi 不固定  非dp项1~r-l+1 
	}
	for(int i=l;i<=m;i++) a[i-l+1]=1ll*dp[i]%mod;//预处理ai 不固定 dp项 l~m
	
	ntt(a,len,1);ntt(b,len,1); 
    for (int i=0;i<len;++i) a[i]=1ll*a[i]*b[i]%mod;
    ntt(a,len,-1);
    
    for(int i=m+1;i<=r;i++) dp[i]=((dp[i]+a[i-l+1])%mod)%mod;//将[l~m]的影响加到dp[m+1~r]上 
    cdq(m+1,r);
}


int main(){
    //n=gi();m=gi();//快读
    //N=max(n,m);//求和表达式 上限   (3.会用到) 
    lim=_-3;//阶乘上限   (非NTT)
    
    //预处理阶乘 跟逆元阶乘 (非NTT)
    jc[0]=1; for (int i=1;i<=lim;++i) jc[i]=1ll*jc[i-1]*i%mod;
    
    //memset(dp,0,sizeof(dp));
    dp[0]=1;
    cdq(0,100000);//预处理DP 
	int T;scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);printf("%d\n",dp[n]);
	}
    return 0;
}

 

### NTT 数论变换的优化方法与实现细节 快速数论变换(NTT)是离散傅里叶变换(DFT)的一种快速算法,其时间复杂度为 $ O(n \log n) $,适用于模数下的多项式运算。NTT 的核心思想是通过模数运算来替代复数运算,从而避免浮点数精度问题。 #### 1. NTT 的基本性质 NTT 的实现依赖于模数下的原根 $ g $,它满足以下性质: - $ g^n = 1 $ - $ g^k \neq 1 $,当 $ 0 < k < n $ 这些性质确保了 $ g $ 可以作为 NTT 中的单位根 $ \omega_n $,类似于 FFT 中的复数单位根。NTT 的核心公式为: $$ A_k = \sum_{i=0}^{n-1} a_i g^{\frac{i(n-k)}{n}} \mod p $$ 其中 $ p $ 是一个大质数,且 $ n $ 是 $ p-1 $ 的因数[^1]。 #### 2. NTT优化方法 为了提高 NTT 的性能,通常采用以下几种优化策略: ##### 2.1 蝴蝶变换(Butterfly Operation) 蝴蝶变换是 NTT 的关键优化点之一,它通过分治策略将递归层数优化为 $ O(\log n) $ 层。每一层的计算可以通过以下公式实现: $$ x_i = a_i + a_{i+n/2} $$ $$ x_{i+n/2} = (a_i - a_{i+n/2}) \cdot \omega_n^k $$ 其中 $ \omega_n^k $ 是当前层的单位根。这种变换减少了重复计算,并且可以利用位反转排列来优化内存访问。 ##### 2.2 位反转排列(Bit-Reversal Permutation) 在 NTT 中,输入序列通常需要进行位反转排列,以确保每一层的计算可以并行进行。位反转排列的核心思想是将索引的二进制表示反转,并重新排列数组。例如,索引 $ 0, 1, 2, 3 $ 在反转后变为 $ 0, 2, 1, 3 $。 ##### 2.3 模数选择 NTT 的性能还依赖于模数 $ p $ 的选择。常见的模数包括 $ p = 998244353 $,因为它是一个大质数,并且 $ p-1 $ 是 $ 2^{23} $ 的倍数,适合处理 $ n $ 为 $ 2^k $ 的情况。选择合适的模数可以确保 NTT 的高效性,并且避免溢出问题。 #### 3. NTT 的实现细节 NTT 的实现通常包括以下几个步骤: ##### 3.1 预处理单位根 在 NTT 开始之前,需要预先计算单位根 $ \omega_n^k $ 及其逆元 $ \omega_n^{-k} $。单位根的计算可以通过原根 $ g $ 实现: $$ \omega_n = g^{\frac{p-1}{n}} $$ $$ \omega_n^{-1} = g^{p-1 - \frac{p-1}{n}} $$ ##### 3.2 递归与迭代实现 NTT 可以通过递归或迭代的方式实现。递归实现较为直观,但迭代实现通常更高效,因为它避免了递归调用的开销。迭代实现通常使用位反转排列来优化内存访问。 ##### 3.3 逆变换 NTT 的逆变换可以通过以下公式实现: $$ a_i = \frac{1}{n} \sum_{k=0}^{n-1} A_k \omega_n^{-ki} \mod p $$ 其中 $ \frac{1}{n} $ 可以通过模逆元计算得到。 #### 4. NTT 的算法改进 为了进一步优化 NTT,可以采用以下改进策略: ##### 4.1 多模数 NTT 多模数 NTT 通过结合多个模数的结果来处理更大的数域,避免单个模数的限制。这种方法通常需要使用中国剩余定理(CRT)来合并结果。 ##### 4.2 混合 NTT 混合 NTT 结合了 NTTFFT 的优点,利用 FFT 的浮点运算和 NTT 的整数运算,以提高精度和性能。 ##### 4.3 并行化 NTT 的每一层计算可以并行化,利用多线程或 GPU 加速来提高性能。例如,使用 OpenMP 或 CUDA 来并行化蝴蝶变换。 #### 示例代码 以下是一个简单的 NTT 实现: ```cpp #include <vector> #include <iostream> using namespace std; const int MOD = 998244353; const int G = 3; int power(int a, int b, int mod) { int res = 1; while (b) { if (b & 1) res = 1LL * res * a % mod; a = 1LL * a * a % mod; b >>= 1; } return res; } void ntt(vector<int> &a, int inv) { int n = a.size(); vector<int> b = a; for (int i = 0; i < n; i++) { int rev = 0; for (int j = 0; j < 20; j++) { if (i & (1 << j)) rev |= (1 << (19 - j)); } if (i < rev) a[i] = b[rev]; } for (int len = 2; len <= n; len <<= 1) { int wn = power(G, (MOD - 1) / len, MOD); if (inv == -1) wn = power(wn, MOD - 2, MOD); for (int i = 0; i < n; i += len) { int w = 1; for (int j = 0; j < len / 2; j++) { int x = a[i + j]; int y = 1LL * a[i + j + len / 2] * w % MOD; a[i + j] = (x + y) % MOD; a[i + j + len / 2] = (x - y + MOD) % MOD; w = 1LL * w * wn % MOD; } } } if (inv == -1) { int inv_n = power(n, MOD - 2, MOD); for (int i = 0; i < n; i++) { a[i] = 1LL * a[i] * inv_n % MOD; } } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值