多项式乘法卡常技巧与任意模数NTT

一.普通多项式乘法.

作为最基础的多项式运算,这里不再介绍,具体参见快速傅里叶变换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=0n1ωnjk(ajibj)=j=0n1(cosX+isinX)(ajibj)=j=0n1((ajcosX+bjsinX)+i(ajsinXbjcosX))=conj(j=0n1((ajcosX+bjsinX)i(ajsinXbjcosX)))=conj(j=0n1((ajcos(X)bjsin(X))+i(ajsin(X)+bjcos(X))))=conj(j=0n1(aj+ibj)(cos(X)+isin(X)))=conj(j=0n1ωnjk(ajibj))=conj(FP(nk))

突然发现真的有关系,那么我们只需要做 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 xx1(modA)xx2(modB)xx3(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+k1Ax2(modB)k1Ax2x1(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) xx1+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+k4ABx3(modC)k4ABx3x4(modC)

那么有 x ≡ x 4 + k 4 A B    ( m o d    A B C ) x\equiv x_4+k_4AB\,\,(mod\,\,ABC) xx4+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();
  }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值