多项式算法的常数问题

wucstdio

2019-03-28 11:15:21

Personal

## 引言 最近几天自学了多项式全家桶。 ![](https://cdn.luogu.com.cn/upload/pic/55203.png) 众所周知,多项式算法的常数都比较巨大。比如多项式多点求值,$n=64000$,时间复杂度$O(n\log^2 n)$,时限竟然开到了3000ms,导致直接暴力卡卡常都能水过去。(卡常卡的好累) WC2019讲课人JOHNKRAM曾经说过,多项式的题目,只需要让暴力跑不过去就够了。 这一篇文章就主要讲述一下多项式算法的常数问题。 因为多项式全家桶已(wo)经(shi)在(zai)日(lan)报(de)里(xie)了,这篇文章几乎不会提及多项式算法的推导过程,所以……不会多项式全家桶的同学请参考[这篇洛谷日报](https://www.luogu.org/blog/user7035/duo-xiang-shi-zong-jie) 先贴一下我的多项式全家桶[代码](https://www.luogu.org/paste/5h43zj92): ### FFT、NTT、FWT 这三种变换可以说是最基础的多项式算法了。 题目链接:[FFT](https://www.luogu.org/problemnew/show/P3803),[FWT](https://www.luogu.org/problemnew/show/P4717)。 ```cpp void FFT(complex*A,int type) { for(int i=0;i<limit;i++) if(i<r[i])swap(A[i],A[r[i]]); for(int mid=1;mid<limit;mid<<=1) { complex Wn(cos(Pi/mid),type*sin(Pi/mid)); for(int R=mid<<1,j=0;j<limit;j+=R) { complex w(1,0); for(int k=0;k<mid;k++,w=w*Wn) { complex x=A[j+k],y=w*A[j+mid+k]; A[j+k]=x+y; A[j+mid+k]=x-y; } } } if(type==-1) { for(int i=0;i<limit;i++) A[i]=A[i]/limit; } } ll omega[2][21][1<<20]; void pre()//预处理单位根,可以显著优化NTT常数 { for(int mid=1,i=0;i<=20;mid<<=1,i++) { ll w1=quick_pow(3,(MOD-1)/(mid<<1)); ll w2=quick_pow(3,MOD-1-(MOD-1)/(mid<<1)); omega[0][i][0]=omega[1][i][0]=1; for(int k=1;k<mid;k++) { omega[1][i][k]=omega[1][i][k-1]*w1%MOD; omega[0][i][k]=omega[0][i][k-1]*w2%MOD; } } } void NTT(ll*A,int type) { for(int i=0;i<limit;i++) if(i<r[i])swap(A[i],A[r[i]]); if(type==-1)type=0; for(int mid=1,i=0;mid<limit;mid<<=1,i++) { for(int R=mid<<1,j=0;j<limit;j+=R) { for(int k=0;k<mid;k++) { ll x=A[j+k]%MOD,y=omega[type][i][k]*A[j+mid+k]%MOD; A[j+k]=x+y; A[j+mid+k]=x-y; } } } for(int i=0;i<limit;i++) { A[i]%=MOD; if(A[i]<0)A[i]+=MOD; } if(type==0) { ll inv=quick_pow(limit,MOD-2); for(int i=0;i<limit;i++)A[i]=A[i]*inv%MOD; } } void FWT_or(ll*A,int type) { for(int mid=1;mid<limit;mid<<=1) for(int R=mid<<1,j=0;j<limit;j+=R) for(int k=0;k<mid;k++) if(type==1)A[j+mid+k]+=A[j+k]; else A[j+mid+k]-=A[j+k]; for(int i=0;i<limit;i++) { A[i]%=MOD; if(A[i]<0)A[i]+=MOD; } } void FWT_and(ll*A,int type) { for(int mid=1;mid<limit;mid<<=1) for(int R=mid<<1,j=0;j<limit;j+=R) for(int k=0;k<mid;k++) if(type==1)A[j+k]+=A[j+mid+k]; else A[j+k]-=A[j+mid+k]; for(int i=0;i<limit;i++) { A[i]%=MOD; if(A[i]<0)A[i]+=MOD; } } void FWT_xor(ll*A,int type) { for(int mid=1;mid<limit;mid<<=1) for(int R=mid<<1,j=0;j<limit;j+=R) for(int k=0;k<mid;k++) { ll x=A[j+k],y=A[j+mid+k]; A[j+k]=x+y; A[j+mid+k]=x-y; if(A[j+k]>MOD)A[j+k]-=MOD; if(A[j+mid+k]<0)A[j+mid+k]+=MOD; if(type==-1) { A[j+k]=A[j+k]*inv2%MOD; A[j+mid+k]=A[j+mid+k]*inv2%MOD; } } } ``` 为了方便,我们假设这三种变换的常数都是$1$,也就是$n\log n$。(当然这些算法的常数也是有的,而且还不小) ### 任意模数FFT(MTT) [题目链接](https://www.luogu.org/problemnew/show/P4245) 共轭优化后,其实本质上就是4次DFT啦:(目前这份代码无论是时间上还是空间上排名都十分靠前,请自动忽略指令集优化的FFT) ```cpp void Mul(ll*A,ll*B,ll*C,ll MOD,complex*a,complex*b) { complex da1,db1,dc1,dd1,da2,db2,dc2,dd2,Da1,Db1,Dc1,Dd1,Da2,Db2,Dc2,Dd2; for(int i=0;i<limit;i++)a[i]=complex(A[i]&M,A[i]>>15); for(int i=0;i<limit;i++)b[i]=complex(B[i]&M,B[i]>>15); FFT(a,1); FFT(b,1); for(int i=0;i<=(limit>>1);i++) { int j=(limit-i)&(limit-1); da1=(a[i]+a[j].conj())*complex(0.5,0); db1=(a[i]-a[j].conj())*complex(0,-0.5); dc1=(b[i]+b[j].conj())*complex(0.5,0); dd1=(b[i]-b[j].conj())*complex(0,-0.5); da2=(a[j]+a[i].conj())*complex(0.5,0); db2=(a[j]-a[i].conj())*complex(0,-0.5); dc2=(b[j]+b[i].conj())*complex(0.5,0); dd2=(b[j]-b[i].conj())*complex(0,-0.5); Da1=da1*dc1; Db1=da1*dd1; Dc1=db1*dc1; Dd1=db1*dd1; Da2=da2*dc2; Db2=da2*dd2; Dc2=db2*dc2; Dd2=db2*dd2; a[i]=Da2+Db2*complex(0,1.0); b[i]=Dc2+Dd2*complex(0,1.0); a[j]=Da1+Db1*complex(0,1.0); b[j]=Dc1+Dd1*complex(0,1.0); } FFT(a,1); FFT(b,1); for(int i=0;i<limit;i++) { ll da=(ll)(a[i].x/limit+0.5)%MOD; ll db=(ll)(a[i].y/limit+0.5)%MOD; ll dc=(ll)(b[i].x/limit+0.5)%MOD; ll dd=(ll)(b[i].y/limit+0.5)%MOD; C[i]=(da+((ll)(db+dc)<<15)+((ll)dd<<30))%MOD; if(C[i]<0)C[i]+=MOD; } } ``` $$T(n)=4n\log n$$ ### 牛顿迭代 牛顿迭代计算复杂度的公式是 $$T(n)=T\left(n\over 2\right)+O(n\log n)=O(n\log n)$$ 让我们认真化一下计算复杂度的式子(我们先假设后面那个$O(n\log n)$的常数为$1$,所有的$\log$都是以$2$为底): $$T(n)=T\left(n\over 2\right)+n\log n$$ $$=n\log n+\dfrac n2 \log\left(\dfrac n2\right)+T\left(n\over 4\right)$$ $$=\cdots=\sum_{i=0}^{\infty}\dfrac{n}{2^i}\log\left(\dfrac{n}{2^i}\right)$$ $$=\sum_{i=0}^{\infty}\dfrac{n}{2^i}\left(\log n-\log 2^i\right)$$ $$=n\log n\left(\sum_{i=0}^{\infty}\dfrac{1}{2^i}\right)-n\sum_{i=0}^{\infty}\dfrac{i}{2^i}$$ $$=2n\log n-2n$$ $$\approx 2n\log n$$ 也就是说,每一次迭代会让后面那个$n\log n$常数乘$2$。(为了方便我们忽略掉一次项) 利用这个式子,我们就可以得到其它多项式算法的常数: ### 多项式求逆 [题目链接](https://www.luogu.org/problemnew/show/P4238) 多项式求逆的原理是牛顿迭代+多项式乘法。 ```cpp void Inv(ll*a,ll*b,int n) { if(n==1) { b[0]=quick_pow(a[0],MOD-2); return; } Inv(a,b,(n+1)>>1);//牛顿迭代 limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<limit;i++)A[i]=i<n?a[i]:0; for(int i=(n+1)>>1;i<limit;i++)b[i]=0; NTT(A,1);//nlogn NTT(b,1);//nlogn for(int i=0;i<limit;i++) b[i]=b[i]*(2+MOD-A[i]*b[i]%MOD)%MOD; NTT(b,-1);//nlogn for(int i=n;i<limit;i++)b[i]=0; } ``` $$T(n)=2T\left(\dfrac{n}{2}\right)+3n\log n=6n\log n$$ 我们可以看到,仅仅一个多项式求逆,常数就是NTT的6倍,多项式算法常数之大也就可想而知了。 ### 多项式开方 [题目链接](https://www.luogu.org/problemnew/show/P5205) 在多项式求逆的基础上,再进行一次牛顿迭代,想想就害怕。 ```cpp void Sqrt(ll*a,ll*b,int n) { if(n==1) { b[0]=1; return; } Sqrt(a,b,(n+1)>>1);//牛顿迭代 Inv(b,inv,n);//6nlogn limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<limit;i++) A[i]=i<n?a[i]:0; NTT(inv,1);//nlogn NTT(A,1);//nlogn for(int i=0;i<limit;i++)inv[i]=inv[i]*A[i]%MOD; NTT(inv,-1);//nlogn for(int i=0;i<limit;i++)b[i]=(b[i]+inv[i])*inv2%MOD; for(int i=n;i<limit;i++)b[i]=0; } ``` $$T(n)=2T\left(\dfrac{n}{2}\right)+6n\log n+3n\log n=18n\log n$$ 常数已经上天了。 ### 多项式求ln: [题目链接](https://www.luogu.org/problemnew/show/P4725) 这个算法不需要牛顿迭代,但是要调用一次多项式求逆和三次NTT: ```cpp void Ln(ll*a,ll*b,int n) { Inv(a,b,n);//6nlogn limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<n-1;i++)a2[i]=a[i+1]*(i+1)%MOD; for(int i=n-1;i<limit;i++)a2[i]=0; NTT(a2,1);//nlogn NTT(b,1);//nlogn for(int i=0;i<limit;i++) b[i]=b[i]*a2[i]%MOD; NTT(b,-1);//nlogn for(int i=n-1;i>0;i--) b[i]=b[i-1]*ni[i]%MOD; for(int i=n;i<limit;i++)b[i]=0; b[0]=0; } ``` $$T(n)=9n\log n$$ ### 多项式exp: [题目链接](https://www.luogu.org/problemnew/show/P4726) 先求多项式ln,然后牛顿迭代: ```cpp void Exp(ll*a,ll*b,int n) { if(n==1) { b[0]=1; return; } Exp(a,b,(n+1)>>1);//牛顿迭代 Ln(b,lnb,n);//9nlogn limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<n;i++)lnb[i]=a[i]>=lnb[i]?a[i]-lnb[i]:a[i]-lnb[i]+MOD; for(int i=n;i<limit;i++)lnb[i]=b[i]=0; lnb[0]++; NTT(lnb,1);//nlogn NTT(b,1);//nlogn for(int i=0;i<limit;i++)b[i]=b[i]*lnb[i]%MOD; NTT(b,-1);//nlogn for(int i=n;i<limit;i++)b[i]=0; } ``` $$T(n)=2T\left(\dfrac{n}{2}\right)+9n\log n+3n\log n=24n\log n$$ (哪些多项式exp跑的比我的NTT还快的,你们是魔鬼吗) ### 多项式三角函数 [题目链接](https://www.luogu.org/problemnew/show/P5264) 本质上就是两遍多项式exp啦: ```cpp void Sin(ll*a,ll*b,int n) { for(int i=0;i<n;i++)a[i]=a[i]*img%MOD; Exp(a,tmp3,n);//24nlogn for(int i=0;i<n;i++)a[i]=MOD-a[i]; Exp(a,b,n);//24nlogn ll inv=inv2*invi%MOD; for(int i=0;i<n;i++) b[i]=(tmp3[i]-b[i]+MOD)*inv%MOD; } void Cos(ll*a,ll*b,int n) { for(int i=0;i<n;i++)a[i]=a[i]*img%MOD; Exp(a,tmp3,n);//24nlogn for(int i=0;i<n;i++)a[i]=MOD-a[i]; Exp(a,b,n);//24nlogn for(int i=0;i<n;i++) b[i]=(tmp3[i]+b[i])*inv2%MOD; } ``` $$T(n)=48n\log n$$ ### 多项式求arcsin [题目链接](https://www.luogu.org/problemnew/show/P5265) 查导数表可知: $$\arcsin'(x)=\dfrac{1}{\sqrt{1-x^2}}$$ 多项式平方+多项式开方+多项式求逆+多项式乘法: ```cpp void Asin(ll*f,ll*g,int n) { for(int i=1;i<n;i++) tmp3[i-1]=f[i]*i%MOD; limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); NTT(f,1);//nlogn for(int i=0;i<limit;i++)f[i]=f[i]*f[i]%MOD; NTT(f,-1);//nlogn for(int i=n;i<limit;i++)f[i]=0; for(int i=0;i<n;i++)f[i]=MOD-f[i]; f[0]++; Sqrt(f,g,n);//18nlogn Inv(g,f,n);//6nlogn limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); NTT(f,1);//nlogn NTT(tmp3,1);//nlogn for(int i=0;i<limit;i++)f[i]=f[i]*tmp3[i]%MOD; NTT(f,-1);//nlogn for(int i=n-1;i>=0;i--)g[i]=f[i-1]*ni[i]%MOD; g[0]=0; } ``` $$T(n)=29n\log n$$ ### 多项式求arctan 一样查导数表: $$\arctan'(x)=\dfrac{1}{1+x^2}$$ 多项式平方+多项式求逆+多项式乘法: ```cpp void Atan(ll*f,ll*g,int n) { for(int i=1;i<n;i++) tmp2[i-1]=f[i]*i%MOD; limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); NTT(f,1);//nlogn for(int i=0;i<limit;i++)f[i]=f[i]*f[i]%MOD; NTT(f,-1);//nlogn for(int i=n;i<limit;i++)f[i]=0; f[0]++; Inv(f,g,n);//6nlogn limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); NTT(g,1);//nlogn NTT(tmp2,1);//nlogn for(int i=0;i<limit;i++)g[i]=g[i]*tmp2[i]%MOD; NTT(g,-1);//nlogn for(int i=n-1;i>=0;i--)g[i]=g[i-1]*ni[i]%MOD; g[0]=0; } ``` 常数稍小一点, $$T(n)=11n\log n$$ ### 多项式带余除法、多项式取模 [题目链接](https://www.luogu.org/problemnew/show/P4512) 一遍求逆+6遍NTT: ```cpp void Div(ll*F,ll*G,ll*Q,ll*R,int n,int m) { for(int i=0;i<(m>>1);i++)swap(G[i],G[m-i-1]); Inv(G,Q,n-m+1);//6nlogn limit=1,l=0; while(limit<(n<<1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<n-m+1;i++)tmp1[i]=F[n-i-1]; for(int i=n-m+1;i<limit;i++)tmp1[i]=0; NTT(Q,1);//nlogn NTT(tmp1,1);//nlogn for(int i=0;i<limit;i++)Q[i]=Q[i]*tmp1[i]%MOD; NTT(Q,-1);//nlogn for(int i=0;i<(m>>1);i++)swap(G[i],G[m-i-1]); for(int i=0;i<((n-m+1)>>1);i++)swap(Q[i],Q[n-m-i]); for(int i=n-m+1;i<limit;i++)Q[i]=0; for(int i=0;i<limit;i++)tmp1[i]=Q[i],R[i]=i<m?G[i]:0; NTT(tmp1,1);//nlogn NTT(R,1);//nlogn for(int i=0;i<limit;i++)R[i]=tmp1[i]*R[i]%MOD; NTT(R,-1);//nlogn for(int i=0;i<m-1;i++)R[i]=(F[i]>=R[i]?F[i]-R[i]:F[i]-R[i]+MOD); for(int i=m-1;i<limit;i++)R[i]=0; } void Mod(ll*F,ll*G,ll*R,int n,int m) { if(n<m) { for(int i=0;i<n;i++)R[i]=F[i]; return; } Div(F,G,tmp2,R,n,m); } ``` $$T(n)=12n\log n$$ ### 主定理的常数 再往下,时间复杂度终于不是$O(n\log n)$了。 由主定理可知, $$T(n)=2T\left(\dfrac n2\right)+O(n\log n)\implies T(n)=O(n\log^2 n)$$ 那么主定理的常数又有多少呢? 一个$T(n)$会变成两个$T\left(\dfrac n2\right)$,所以我们可以将复杂度画成一个树形结构: ![](https://cdn.luogu.com.cn/upload/pic/55207.png) 容易发现最终的复杂度就是最后一列所有的数加起来。 $$\sum_{i=0}^{\log n}n\log \dfrac{n}{2^i}$$ $$=n\sum_{i=0}^{\log n}(\log n-\log 2^i)$$ $$=n\log^2 n-n\sum_{i=0}^{\log n}i$$ $$=n\log^2 n-n\dfrac{\log n(\log n+1)}{2}$$ $$\approx\dfrac{1}{2}n\log^2 n$$ 也就是说,一次主定理会让复杂度多一个$\log$,但是常数减半。注意这里的常数如果转化到一个$\log$的话大概还要乘$20$左右。 ### 多项式多点求值 [题目链接](https://www.luogu.org/problemnew/show/P5050) 多项式多点求值的思路是什么呢? $$f(x_i)=f(x)\bmod{\:(x-x_i)}$$ 我们需要先用分治求出$\prod_{i=l}^r(x-x_i)$,然后再分治取模。 这样的时间复杂度分为两部分: ```cpp void solve(int o,int L,int R,ll*x) { if(L==R) { _p[o]=__p+tot; tmp3[o]=2; tot+=2; _p[o][0]=MOD-x[L]; _p[o][1]=1; return; } int mid=(L+R)>>1; solve(o<<1,L,mid,x); solve(o<<1|1,mid+1,R,x);//2T(n/2) limit=1,l=0; while(limit<=(R-L+1))limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<tmp3[o<<1];i++)tmp1[i]=_p[o<<1][i]; for(int i=tmp3[o<<1];i<limit;i++)tmp1[i]=0; for(int i=0;i<tmp3[o<<1|1];i++)tmp2[i]=_p[o<<1|1][i]; for(int i=tmp3[o<<1|1];i<limit;i++)tmp2[i]=0; NTT(tmp1,1);//nlogn NTT(tmp2,1);//nlogn for(int i=0;i<limit;i++)tmp1[i]=tmp1[i]*tmp2[i]%MOD; NTT(tmp1,-1);//nlogn while(limit>1&&tmp1[limit-1]==0)limit--; _p[o]=__p+tot; tmp3[o]=limit; tot+=limit; for(int i=0;i<limit;i++)_p[o][i]=tmp1[i]; } void query(int o,int dep,int l,int r,ll*ans) { if(l==r) { ans[l]=_f[dep][0]; return; } int mid=(l+r)>>1; Mod(_f[dep],_p[o<<1],_f[dep+1],tmp3[o],tmp3[o<<1]);//12nlogn query(o<<1,dep+1,l,mid,ans);//T(n/2) Mod(_f[dep],_p[o<<1|1],_f[dep+1],tmp3[o],tmp3[o<<1|1]);//12nlogn query(o<<1|1,dep+1,mid+1,r,ans);//T(n/2) } void Eval(ll*a,ll*x,int n,int m,ll*res) { solve(1,1,m,x); Mod(a,_p[1],_f[1],n,tmp3[1]); query(1,1,1,m,res); } ``` $$T(n)=T_1(n)+T_2(n)=\left(2T_1\left(\dfrac n2\right)+3n\log n\right)+\left(2T_2\left(\dfrac n2\right)+24n\log n\right)$$ $$T(n)=1.5n\log^2 n+12n\log^2 n=13.5n\log^2 n$$ $n=64000$,时限3000ms,就是这么来的,注意这里还忽略了$n\log n$的项。 ### 多项式快速插值 (啥快速啊,没准还不如暴力快) [题目链接](https://www.luogu.org/problemnew/show/P5158) 多项式快速插值的原理是拉格朗日插值公式,也就是 $$A(x)=\sum_{i=1}^ny_i\dfrac{\prod_{j\neq i}(x-x_j)}{\prod_{j\neq i}(x_i-x_j)}$$ 注意到$\prod_{j\neq i}(x-x_j)=\pi'(x_i)$,其中$\pi(x)=\prod_{i=1}^n(x-x_i)$。 所以我们先用分治乘法求出$\pi(x)$,求导后多点求值,最后再用一个分治乘法求出分母部分。 这样做的常数问题嘛……还是先上代码: ```cpp void ask(int o,int dep,int L,int R,ll*y) { if(L==R) { _f[dep][0]=y[L]; return; } int mid=(L+R)>>1; ask(o<<1,dep+1,L,mid,y);//T(n/2) limit=1,l=0; while(limit<R-L+1)limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<mid-L+1;i++)tmp1[i]=_f[dep+1][i]; for(int i=mid-L+1;i<limit;i++)tmp1[i]=0; for(int i=0;i<tmp3[o<<1|1];i++)tmp2[i]=_p[o<<1|1][i]; for(int i=tmp3[o<<1|1];i<limit;i++)tmp2[i]=0; NTT(tmp1,1);//nlogn NTT(tmp2,1);//nlogn for(int i=0;i<limit;i++)_f[dep][i]=tmp1[i]*tmp2[i]; ask(o<<1|1,dep+1,mid+1,R,y);//T(n/2) limit=1,l=0; while(limit<R-L+1)limit<<=1,l++; for(int i=0;i<limit;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1)); for(int i=0;i<tmp3[o<<1];i++)tmp1[i]=_p[o<<1][i]; for(int i=tmp3[o<<1];i<limit;i++)tmp1[i]=0; for(int i=0;i<R-mid;i++)tmp2[i]=_f[dep+1][i]; for(int i=R-mid;i<limit;i++)tmp2[i]=0; NTT(tmp1,1);//nlogn NTT(tmp2,1);//nlogn for(int i=0;i<limit;i++)_f[dep][i]=(_f[dep][i]+tmp1[i]*tmp2[i])%MOD; NTT(_f[dep],-1);//nlogn } void Inter(ll*x,ll*y,int n,ll*res) { solve(1,1,n,x);//求出\pi(x) for(int i=1;i<tmp3[1];i++) res[i-1]=_p[1][i]*i%MOD; for(int i=tmp3[1]-1;i<tmp3[1]<<2;i++)res[i]=0; Mod(res,_p[1],_f[1],tmp3[1]-1,tmp3[1]); query(1,1,1,n,res);//带入多点求值 for(int i=1;i<=n;i++)res[i]=y[i]*quick_pow(res[i],MOD-2)%MOD; ask(1,1,1,n,res);//最后再分治乘一波 for(int i=0;i<n;i++)res[i]=_f[1][i]%MOD; } ``` $$T(n)=T_1(n)+T_2(n)=13.5n\log^2 n+\left(T_2\left(\dfrac{n}{2}\right)+5n\log n\right)=16n\log^2 n$$ ## 总结 总之就是常数大。 由于忽略了所有的低阶项,所以这些算法的常数会比理论计算要大一些。 理论要有实践来支撑。在Linux虚拟机里面运行的结果如下(没有开O2,三次测量四舍五入取平均): | 算法 | 理论复杂度 | $n=100000$ | 与NTT的比值 | $n=1000000$ | 与NTT的比值 | | :----------: | :----------: | :----------: | :----------: | :----------: | :----------: | | NTT | $n\log n$ | 50ms | 1 | 500ms | 1 | | FFT | $n\log n$ | 124ms | 2.48 | 900ms | 1.8 | | FWT(or) | $n\log n$ | 40ms | 0.8 | 170ms | 0.34 | | FWT(and) | $n\log n$ | 50ms | 1 | 175ms | 0.35 | | FWT(xor) | $n\log n$ | 70ms | 1.4 | 330ms | 0.66 | | MTT | $4n\log n$ | 870ms | 17.4 | 7620ms | 15.24 | | 多项式求逆 | $6n\log n$ | 528ms | 10.56 | 4680ms | 9.36 | | 多项式开方 | $18n\log n$ | 1404ms | 28.08 | 13300ms | 26.6 | | 多项式$\ln$ | $9n\log n$ | 770ms | 15.4 | 7150ms | 14.3 | | 多项式$\exp$ | $24n\log n$ | 1860ms | 37.2 | 17800ms | 35.6 | | 多项式$\sin$ | $48n\log n$ | 3680ms | 73.6 | 35100ms | 70.2 | | 多项式$\cos$ | $48n\log n$ | 3690ms | 73.8 | 35200ms | 70.4 | | 多项式$\arcsin$ | $29n\log n$ | 2290ms | 45.8 | 21700ms | 43.4 | | 多项式$\arctan$ | $11n\log n$ | 940ms | 18.8 | 8650ms | 17.3 | | 多项式带余除法 | $12n\log n$ | 760ms | 15.2 | 7000ms | 14 | | 多项式多点求值 | $13.5n\log n$ | 15400ms | 308 | 175000ms | 350 | | 多项式快速插值 | $16n\log n$ | 17400ms | 348 | 196500ms | 393 | (你能想象3分钟一个点的**快速**插值吗) 由于虚拟机的运行速度可想而知,以上所有运行时间参考意义都不大,放到洛谷上大概要除以8(我的快速插值在洛谷上$100000$的数据最慢的点跑了2000ms) 所有运行时间小于1000ms的误差都相当大,而且我的NTT前面加了一个预处理的常数,所以前面FFT和FWT与NTT运行时间的比值会有较大的误差。 可以发现在实际运行中,所有算法与NTT的相对常数大概还要再乘$1.5$左右,其中任意模数NTT更是比理论复杂度慢了2倍(与FFT相比)。但是随着$n$的增大,时间复杂度相同的情况下,绝大多数比值在逐渐减小,这也符合渐进复杂度的规律。 ## 多项式常数大,慎用!