一.普通多项式乘法.
作为最基础的多项式运算,这里不再介绍,具体参见快速傅里叶变换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<ABC
x≡x1(modA)x≡x2(modB)x≡x3(modC)x<ABC
先合并
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<ABC x<ABC,所以 x = x 4 + k 4 A B = x 1 + k 1 A + k 4 A B x=x_4+k_4AB=x_1+k_1A+k_4AB x=x4+k4AB=x1+k1A+k4AB.
直接在模 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();
}
}