多项式全家桶
多项式乘法
多项式乘法$CX=AX\times BX$中的系数满足加法卷积$c_i=\Sigma a_j b_k [i==j+k]$,可以将一个乘法看作卷积,反之也是。
一般来说,多项式直接相乘的时间复杂度都是$O(n^2)$级别的,使用压位等技巧也只能做到常数上的优化,面对更大的数据规模就捉襟见肘了。
既然是卷积,就可以考虑卷积通用的方法进行优化。如果能找到映射$f$,使得在$f$下点乘的性质成立,并且可求逆运算,即
就有可能找到更好的做法。
快速傅里叶变换
傅里叶变换是一个物理上的方法,虽然他们名字相同,但是不明白他和这个过程有什么使用上的联系。得知复数单位根有着优良的性质,自然想到要用他来优化多项式乘法
对于多项式
可以把他进行奇偶分解,记奇偶子式分别为$h(x),g(x)$
代入$x_i=\omega_n^i$,
这个求$f$的过程是典型的折半递归,得到了一个时间复杂度为$O(nlogn)$的求解法。求出了映射$A\rightarrow f(A)$,可以在线性的时间内实现点乘,接下来考虑逆变换$f(C)\rightarrow C$。
如果把dft过程看作一个矩阵乘法,那么idft就是矩阵的逆。其实用一个更好理解的想法,再次代入$\omega^{-1}$就逆映射。需要注意,直接代入求得的并不是原序列,由线代的知识可知,$A^{-1}=\frac{A^*}{|A|}$,需要再除以序列的长度n。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void fft(int n,cpl *a,int op){ if(n==1)return ; for(int i=0;i<n;i+=2)temp[i/2]=a[i], temp[i/2+n/2]=a[i+1]; ffor(i,0,n-1)a[i]=temp[i]; fft(n/2,a,op),fft(n/2,a+n/2,op); cpl Wn={cos(2.0*PI/n),sin(2.0*PI/n)*op},w=1; for(int i=0,nl=n/2;i<nl;++i,w=w*Wn){ cpl t1=a[i],t2=w*a[i+nl]; a[i]=t1+t2; a[i+nl]=t1-t2; } } void mult(){ fft(n,a1,1),fft(n,a2,1); ffor(i,0,n-1)a3[i]=a1[i]*a2[i]; fft(n,a3,-1); ffor(i,0,nout)a3[i]=int(a3[i].real()/n+0.5); }
|
迭代fft
考虑在$f(a_i)$中所有有贡献的元素,可以发现他们的下标都等于$i>>k$。在$f(\omega)=g(\omega^2)+\omega h(\omega^2)$划分递归的过程,x也在作二进制下的右移。
如果能提前得到在$f(a_i)$所涉及的位置,就不需要递归,可以剩下栈开销与优化常数。
自底而顶地合并元素,可以发现当$i=rev[i]$时,满足fft的性质,其中
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void fft(cpl *a,int op){ ffor(i,0,n-1)if(i<rev[i])swap(a[i],a[rev[i]]); for(int mid=1;mid<n;mid<<=1){ cpl Wn={cos(PI/mid),sin(PI/mid)*op}; for(int p=0;p<n;p+=(mid<<1)){ cpl w=1; for(int k=0;k<mid;++k,w=w*Wn){ cpl t1=a[p+k],t2=w*a[mid+p+k]; a[p+k]=t1+t2; a[mid+p+k]=t1-t2; } } } }
|
快速数论变换
fft使用了复数,有运行常数和精度的劣势,而且许多题目要对998244353取模,一个基于整数的变换算法会更合适。
由数论知识可知,对某些质数$P$,存在原根$g$,使得
那么就可以利用类似于上述的过程来得到变换
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void ntt(ll *a,int op){ ffor(i,0,n-1)if(i<rev[i])swap(a[i],a[rev[i]]); for(int mid=1;mid<n;mid<<=1){ ll Wn=fsp((op==1?G:Gi),(P-1)/(mid<<1)); for(int p=0;p<n;p+=(mid<<1)){ ll w=1; for(int k=0;k<mid;++k,w=w*Wn%P){ ll t1=a[p+k],t2=w*a[mid+p+k]%P; a[p+k]=(t1+t2)%P; a[mid+p+k]=(t1-t2+P)%P; } } } } void mult(){ ntt(a1,1),ntt(a2,1); ffor(i,0,n-1)a3[i]=a1[i]*a2[i]%P; ntt(a3,-1); ffor(i,0,nout)a3[i]=a3[i]*fsp(n)%P; }
|
多项式运算
求导,积分
对于一个多项式函数,求导和积分的原理都非常简单。在计算机中的实现就是移动项并变换相应系数。
在ntt的做法中,可以预处理阶乘和逆阶乘。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ll fac[N],ifac[N];
int deviate(ll *a,int n,int k=1){ ffor(i,0,n-k-1)a[i]=a[i+k]*fac[i+k]%P*ifac[i]%P; return n-k; } int integrate(ll *a,int n,int k=1){ rfor(i,n-1,0)a[i+k]=a[i]*ifac[i+k]%P*fac[i]%P; ffor(i,0,k-1)a[i]=0; return n+k; } void init(){ fac[0]=ifac[0]=1; ffor(i,1,N-1)fac[i]=fac[i-1]*i%P; ifac[N-1]=fsp(fac[N-1]); rfor(i,N-1,1)ifac[i-1]=ifac[i]*i%P; }
|
求逆
$F(x)$的多项式逆元定义为$F(x)G(x)\equiv 1 (mod x^n)$。这个逆元只针对原多项式长度范围内的部分,对于乘积超出的部分直接忽略。
考虑递归求解。在递归边界$n=1$,$G_0=f_0^{-1}$。
在模$x^{\frac{n}{2}}$的意义下,定义$F(X)$的逆多项式$H(x)$。$H(x)$通过递归逐次求解。
可以看出,在低位$x^{\frac{n}{2}}$以下,$H(X)=G(X)$,即$H(X)$就是$G(X)$的低位部分。至于高位部分,考虑平方
这样,通过$\frac{n}{2}$项式$H(X)$,就可以得到$n$项式$G(X)$。
根据fft的经验,二进制分治的递归可以改写成迭代,求逆也可以改用迭代,这样时空开销更小。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ll inv_f[N]; void poly_inv(ll *f,ll *g,int n){ ffor(i,0,n-1)g[i]=0;g[0]=fsp(f[0]); for(ll len=1;len<(n<<1);len<<=1){ ll lim=len<<1; ffor(i,0,lim-1)inv_f[i]=0; ffor(i,0,len-1)inv_f[i]=f[i]; ntt(inv_f,lim),ntt(g,lim); ffor(i,0,lim-1)g[i]=(P+2ll-inv_f[i]*g[i]%P)%P*g[i]%P; ntt(g,lim,-1); ffor(i,len,lim-1)g[i]=0; } }
|
开根
$F(x)$的根式定义为$G^2(x)\equiv F(x) (mod x^n)$。按照求逆的套路,考虑分治合并。
当$H(x)$是$F(x)$在一半长度下的根式
递归,求$H(x)$的逆元,就可由半长根式得全长根式
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ll sqrt_f[N],sqrt_inv[N]; void poly_sqrt(ll *f,ll *g,int n){ ffor(i,0,n-1)g[i]=0;g[0]=1; for(ll len=1;len<(n<<1);len<<=1){ ll lim=len<<1; ffor(i,0,lim-1)sqrt_f[i]=sqrt_inv[i]=0; ffor(i,0,len-1)sqrt_f[i]=f[i]; poly_inv(g,sqrt_inv,len); ntt(sqrt_f,lim),ntt(sqrt_inv,lim); ffor(i,0,lim-1)sqrt_f[i]=sqrt_inv[i]*sqrt_f[i]%P; ntt(sqrt_f,lim,-1); ffor(i,0,len-1)g[i]=(sqrt_f[i]+g[i])%P*inv2%P; ffor(i,len,lim-1)g[i]=0; } }
|