一.普通多项式乘法.
作为最基础的多项式运算,这里不再介绍,具体参见快速傅里叶变换FFT与快速数论变换NTT入门.
这里的多项式乘法卡常技巧不会过于深入,且只适用于FFT.
二.多项式乘法优化1.
这是一种非常简单的多项式乘法常数优化,对于两个系数为实数的多项式 A ( x ) , B ( x ) A(x),B(x) A(x),B(x),我们写成一个多项式 P ( x ) P(x) P(x):
P ( x ) = A ( x ) + i B ( x ) P(x)=A(x)+iB(x) P(x)=A(x)+iB(x)
然后将 P ( x ) P(x) P(x)平方得到:
P 2 ( x ) = ( A ( x ) + i B ( x ) ) 2 = A 2 ( x ) − B 2 ( x ) + 2 i A ( x ) B ( x ) P^{2}(x)=(A(x)+iB(x))^{2}\\ =A^{2}(x)-B^{2}(x)+2iA(x)B(x) P2(x)=(A(x)+iB(x))2=A2(x)−B2(x)+2iA(x)B(x)
我们发现只需要取出 P ( x ) P(x) P(x)的虚部除 2 2 2就是两个多项式卷积的值了,这样我们需要计算DFT的次数从 3 3 3次变成了 2 2 2次.
代码如下:
comp ta[N+9];
void Poly_mul(int *a,int *b,int n){
Get_len(n);
for (int i=0;i<len;++i) ta[i]=comp(a[i],b[i]),b[i]=0;
FFT(ta,len,0);
for (int i=0;i<len;++i) ta[i]=ta[i]*ta[i]*0.5;
FFT(ta,len,1);
for (int i=0;i<len;++i) a[i]=(int)(ta[i].y+0.5),ta[i]=comp();
}
但是这种算法只能直接得到乘积,并不能得到每一个多项式对应的点值,这导致它很不实用.
三.多项式乘法优化2.
对于多项式 A ( x ) , B ( x ) A(x),B(x) A(x),B(x),我们设:
P ( x ) = A ( x ) + i B ( x ) Q ( x ) = A ( x ) − i B ( x ) P(x)=A(x)+iB(x)\\ Q(x)=A(x)-iB(x) P(x)=A(x)+iB(x)Q(x)=A(x)−iB(x)
设 F P ( k ) F_P(k) FP(k)表示对多项式 P ( x ) P(x) P(x)代入 ω n k \omega_{n}^{k} ωnk后的点值,那么有:
F A ( k ) = F P ( k ) + F Q ( k ) 2 F B ( k ) = − i F P ( k ) − F Q ( k ) 2 F_A(k)=\frac{F_P(k)+F_Q(k)}{2}\\ F_B(k)=-i\frac{F_P(k)-F_Q(k)}{2} FA(k)=2FP(k)+FQ(k)FB(k)=−i2FP(k)−FQ(k)
现在我们把 A ( x ) , B ( x ) A(x),B(x) A(x),B(x)两个毫不相关的多项式的点值求解转化为了 P ( x ) , Q ( x ) P(x),Q(x) P(x),Q(x)这两个看起来非常有关的多项式点值求解,这意味着有可能只要求出 P ( x ) P(x) P(x)的点值就能直接得到 Q ( x ) Q(x) Q(x)的点值.
来试试看,设 X = 2 π j k n X=\frac{2\pi jk}{n} X=n2πjk,用 c o n j ( z ) conj(z) conj(z)表示 z z z的共轭复数,那么有:
F Q ( k ) = ∑ j = 0 n − 1 ω n j k ( a j − i b j ) = ∑ j = 0 n − 1 ( cos X + i sin X ) ( a j − i b j ) = ∑ j = 0 n − 1 ( ( a j cos X + b j sin X ) + i ( a j sin X − b j cos X ) ) = c o n j ( ∑ j = 0 n − 1 ( ( a j cos X + b j sin X ) − i ( a j sin X − b j cos X ) ) ) = c o n j ( ∑ j = 0 n − 1 ( ( a j cos ( − X ) − b j sin ( − X ) ) + i ( a j sin ( − X ) + b j cos ( − X ) ) ) ) = c o n j ( ∑ j = 0 n − 1 ( a j + i b j ) ( cos ( − X ) + i sin ( − X ) ) ) = c o n j ( ∑ j = 0 n − 1 ω n − j k ( a j − i b j ) ) = c o n j ( F P ( n − k ) ) F_Q(k)=\sum_{j=0}^{n-1}\omega_{n}^{jk}(a_j-ib_j)\\ =\sum_{j=0}^{n-1}(\cos X+i\sin X)(a_j-ib_j)\\ =\sum_{j=0}^{n-1}((a_j\cos X+b_j\sin X)+i(a_j\sin X-b_j\cos X))\\ =conj\left(\sum_{j=0}^{n-1}((a_j\cos X+b_j\sin X)-i(a_j\sin X-b_j\cos X))\right)\\ =conj\left(\sum_{j=0}^{n-1}((a_j\cos (-X)-b_j\sin (-X))+i(a_j\sin (-X)+b_j\cos (-X)))\right)\\ =conj\left(\sum_{j=0}^{n-1}(a_j+ib_j)(\cos(-X)+i\sin(-X))\right)\\ =conj\left(\sum_{j=0}^{n-1}\omega_{n}^{-jk}(a_j-ib_j)\right)\\ =conj(F_P(n-k)) FQ(k)=j=0∑n−1ωnjk(aj−ibj)=j=0∑n−1(cosX+isinX)(aj−ibj)=j=0∑n−1((ajcosX+bjsinX)+i(ajsinX−bjcosX))=conj(j=0∑n−1((ajcosX+bjsinX)−i(ajsinX−bjcosX)))=conj(j=0∑n−1((ajcos(−X)−bjsin(−X))+i(ajsin(−X)+bjcos(−X))))=conj(j=0∑n−1(aj+ibj)(cos(−X)+isin(−X)))=conj(j=0∑n−1ωn−jk(aj−ibj))=conj(FP(n−k))
突然发现真的有关系,那么我们只需要做 1 1 1次FFT就可以得到 2 2 2个多项式的DFT了,仍然是从 3 3 3次DFT变为 2 2 2次DFT.
代码如下:
comp ta[N+9],tb[N+9];
void Poly_mul(int *a,int *b,int n){
Get_len(n);
for (int i=0;i<len;++i) ta[i]=comp(a[i],b[i]),b[i]=0;
FFT(ta,len,0);
tb[0]=comp(ta[0].x,-ta[0].y);
for (int i=1;i<len;++i) tb[len-i]=comp(ta[i].x,-ta[i].y);
for (int i=0;i<len;++i) ta[i]=comp(0,0.25)*(ta[i]+tb[i])*(tb[i]-ta[i]),tb[i]=comp();
FFT(ta,len,1);
for (int i=0;i<len;++i) a[i]=(int)(ta[i].x+0.5),ta[i]=comp();
}
四.三模数NTT求解任意模数NTT.
对于一个模数 p p p,若要直接用一般NTT求在模 p p p意义下的多项式卷积,由于原根与分治过程需要的特性,模数必须满足可以被分解为 p = 2 k c + 1 p=2^{k}c+1 p=2kc+1,其中 k k k必须足够大,到达 2 k 2^{k} 2k超过多项式项数的程度.
但是若 p p p并不满足这个性质该如何解决问题?
一般来说, p p p在 1 0 9 10^9 109级别左右, n n n在 1 0 5 10^5 105级别左右,所以最终答案序列的每一位会达到 1 0 23 10^{23} 1023级别大小,显然是不太可以用C++中内置的整数和实数类型操作的(__int128
是C++11的).
一种方法是找到三个 1 0 9 10^9 109级别的NTT模数 A , B , C A,B,C A,B,C,分别求出乘积的每一位系数在模 A , B , C A,B,C A,B,C意义下的值,设为 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3,那么对于真正的系数 x x x有:
x ≡ x 1 ( m o d A ) x ≡ x 2 ( m o d B ) x ≡ x 3 ( m o d C ) x < A B C x\equiv x_1\,\,(mod\,\,A)\\ x\equiv x_2\,\,(mod\,\,B)\\ x\equiv x_3\,\,(mod\,\,C)\\ x
先合并 x 1 , x 2 x_1,x_2 x1,x2,有:
x 1 + k 1 A ≡ x 2 ( m o d B ) k 1 ≡ x 2 − x 1 A ( m o d B ) x_1+k_1A\equiv x_2\,\,(mod\,\,B)\\ k_1\equiv \frac{x_2-x_1}{A}\,\,(mod\,\,B) x1+k1A≡x2(modB)k1≡Ax2−x1(modB)
于是我们现在求得 k 1 k_1 k1,并且有形式 x ≡ x 1 + k 1 A ( m o d A B ) x\equiv x_1+k_1A\,\,(mod\,\,AB) x≡x1+k1A(modAB),记 x 4 = x 1 + k 1 A x_4=x_1+k_1A x4=x1+k1A,继续合并 x 4 , x 3 x_4,x_3 x4,x3:
x 4 + k 4 A B ≡ x 3 ( m o d C ) k 4 ≡ x 3 − x 4 A B ( m o d C ) x_4+k_4AB\equiv x_3\,\,(mod\,\,C)\\ k_4\equiv \frac{x_3-x_4}{AB}\,\,(mod\,\,C) x4+k4AB≡x3(modC)k4≡ABx3−x4(modC)
那么有 x ≡ x 4 + k 4 A B ( m o d A B C ) x\equiv x_4+k_4AB\,\,(mod\,\,ABC) x≡x4+k4AB(modABC),由于 x < A B C x
直接在模 p p p意义下做这个式子即可.
由于这个方法并不是非常好用(至少我这么觉得),所以就不贴代码了.
五.拆位FFT求解任意模数NTT.
一般情况下,double
存 1 0 18 10^{18} 1018级别的数还是没什么问题的,所以我们可以尝试拆位分别做FFT.
我们将多项式 A ( x ) A(x) A(x)拆成 A ( x ) = 2 15 A 0 ( x ) + A 1 ( x ) A(x)=2^{15}A_0(x)+A_1(x) A(x)=215A0(x)+A1(x)的形式,那么多项式 A 0 ( x ) , A 1 ( x ) A_0(x),A_1(x) A0(x),A1(x)的系数均在一个相对原来小了不少的范围内.
对 A ( x ) A(x) A(x)和 B ( x ) B(x) B(x)这么处理后,我们写出最后乘积的式子:
A ( x ) B ( x ) = 2 30 A 0 ( x ) B 0 ( x ) + 2 15 ( A 0 ( x ) B 1 ( x ) + A 1 ( x ) B 0 ( x ) ) + A 1 ( x ) B 1 ( x ) A(x)B(x)=2^{30}A_0(x)B_0(x)+2^{15}(A_0(x)B_1(x)+A_1(x)B_0(x))+A_1(x)B_1(x) A(x)B(x)=230A0(x)B0(x)+215(A0(x)B1(x)+A1(x)B0(x))+A1(x)B1(x)
接下来的问题就是分别计算这 4 4 4个卷积了,看起来这 4 4 4个卷积依次实现需要 12 12 12次DFT啊…
但是我们不需要依次实现,事实上,我们只要分别跑出 4 4 4个多项式的DFT,计算出每一个卷积后再IDFT回来就行了,其中系数为 2 15 2^{15} 215的两个卷积可以直接加好再DFT回来,这样只需要 7 7 7次DFT.
然而看起来这和上面的三模数NTT的次数差不了多少啊,而且FFT还有复数运算的常数因子在…
但是别忘了,FFT是可以进行两两合并的!利用多项式乘法优化2中的技巧,DFT的次数降成了 4 4 4次,常数远远小于三模数NTT(实测是一半)!
代码如下:
comp ta[N+9],tb[N+9],tc[N+9],te[N+9];
void Split_fft(comp *a,comp *b){
b[0]=comp(a[0].x,-a[0].y);
for (int i=1;i<len;++i) b[i]=comp(a[len-i].x,-a[len-i].y);
for (int i=0;i<len;++i){
comp x=a[i],y=b[i];
a[i]=(x+y)*0.5;b[i]=comp(0,0.5)*(y-x);
}
}
void Poly_mul(int *a,int *b,int n){
Get_len(n);
for (int i=0;i<len;++i){
ta[i]=comp(a[i]>>15,a[i]&(1<<15)-1);
tc[i]=comp(b[i]>>15,b[i]&(1<<15)-1);
}
FFT(ta,len,0);
FFT(tc,len,0);
Split_fft(ta,tb);
Split_fft(tc,te);
for (int i=0;i<len;++i){
ta[i]=ta[i]*tc[i]+comp(0,1)*ta[i]*te[i];
tb[i]=tb[i]*tc[i]+comp(0,1)*tb[i]*te[i];
}
FFT(ta,len,1);
FFT(tb,len,1);
for (int i=0;i<len;++i){
LL x=(LL)(ta[i].x+0.5)%mod,y=(LL)(ta[i].y+tb[i].x+0.5)%mod,z=(LL)(tb[i].y+0.5)%mod;
a[i]=((x<<30)+(y<<15)+z)%mod;
ta[i]=tb[i]=comp();
}
}