引言
最近几天自学了多项式全家桶。
众所周知,多项式算法的常数都比较巨大。比如多项式多点求值, n = 64000 n=64000 n=64000,时间复杂度 O ( n log 2 n ) O(n\log^2 n) O(nlog2n),时限竟然开到了3000ms,导致直接暴力卡卡常都能水过去。(卡常卡的好累)
WC2019讲课人JOHNKRAM曾经说过,多项式的题目,只需要让暴力跑不过去就够了。
这一篇文章就主要讲述一下多项式算法的常数问题。
因为多项式全家桶已(wo)经(shi)在(zai)日(lan)报(de)里(xie)了,这篇文章几乎不会提及多项式算法的推导过程,所以……不会多项式全家桶的同学请参考这篇洛谷日报
先贴一下我的多项式全家桶代码:
FFT、NTT、FWT
这三种变换可以说是最基础的多项式算法了。
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 1 1,也就是 n log n n\log n nlogn。(当然这些算法的常数也是有的,而且还不小)
任意模数FFT(MTT)
共轭优化后,其实本质上就是4次DFT啦:(目前这份代码无论是时间上还是空间上排名都十分靠前,请自动忽略指令集优化的FFT)
void Mul(ll*A,ll*B,ll*C,ll MOD,complex*a,complex*b)//4nlogn
{
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 ) = 4 n log n T(n)=4n\log n T(n)=4nlogn
牛顿迭代
牛顿迭代计算复杂度的公式是
T ( n ) = T ( n 2 ) + O ( n log n ) = O ( n log n ) T(n)=T\left(n\over 2\right)+O(n\log n)=O(n\log n) T(n)=T(2n)+O(nlogn)=O(nlogn)
让我们认真化一下计算复杂度的式子(我们先假设后面那个 O ( n log n ) O(n\log n) O(nlogn)的常数为 1 1 1,所有的 log \log log都是以 2 2 2为底):
T ( n ) = T ( n 2 ) + n log n T(n)=T\left(n\over 2\right)+n\log n T(n)=T(2n)+nlogn
= n log n + n 2 log ( n 2 ) + T ( n 4 ) =n\log n+\dfrac n2 \log\left(\dfrac n2\right)+T\left(n\over 4\right) =nlogn+