前置知识
复数
多项式
形如
F(x)=i=0∑naixi
的式子。其中 n 为非负整数。我们只需知道所有 ai,就可以确定一个多项式。这就是多项式的系数表示法。
为了方便运算,我们引入多项式的点值表示法。
对于一个 n 次多项式 F(x),我们可以用 n+1 个互不相同的点 {(x0,F(x0)),(x1,F(x1)),⋯,(xn+1,F(xn+1))} 来确定这个多项式。我们下文称这个集合为 SF(x)。
系数表示法转为点值表示法的过程叫做 DFT,反之叫 IDFT。
卷积
H(x)=F(x)×G(x) 表示 H(x) 为 F(x) 和 G(x) 的卷积。
刚才的多项式乘法求得的式子并不是标准的多项式形式。我们设 H(x)=F(x)×G(x)=∑k=0n+mckxk。则每项的系数 ck=∑i+j=kaibj,变为更容易计算的形式
ck=i=max(0,k−m)∑min(n,k)aibk−i
若使用点值表示法,设
SF(x)={(xi,yi)}
SG(x)={(xi,yi′)}
则有
SH(x)={(xi,yiyi′)}
单位根
根据代数基本定理,xn=1 有 n 个根,这 n 个根都称为单位根。记作 {ωnk∣k=0,1,⋯,n−1},其中,ωn0=1。在复平面上,它们刚好将单位圆 n 等分。一般说的单位根 ωn,指从 (1,0) 开始逆时针方向上的第一个根。
一些下面会用到的小式子:
- 对于偶数次单位根,有 ωni=−ωni+2n(其实就是在复平面上关于原点中心对称)。
- ω2n2k=ωnk
- (ωnk)2=ωn2k
显然,朴素算法求解 F(x)∗G(x) 的时间复杂度为 O(nm),而 FFT 可以让我们在 O(nlogn) 的时间复杂度内计算两个 n 次多项式的乘法。基本思想是分治。
我们只需对 F(x) 和 G(x) 进行 DFT,计算 SH(x)=SF(x)∗SG(x),最后再对 SH(x) IDFT 即得 H(x)。
接下来说 DFT 的过程。
对于 F(x),将其划分为奇次与偶次两部分。
F(x)=i=0∑2na2ix2i+i=0∑2na2i+1x2i+1
将右半部分提出一个 x
F(x)=i=0∑2na2ix2i+xi=0∑2na2i+1x2i
将前后两部分用新的多项式表示
F1(x)=i=0∑2na2ixi
F2(x)=i=0∑2na2i+1xi
F(x)=F1(x2)+xF2(x2)
这时我们代入 ωnk,可得
F(ωnk)=F1((ωnk)2)+ωnkF2((ωnk)2)=F1(ωn2k)+ωnkF2(ωn2k)=F1(ω2nk)+ωnkF2(ω2nk)
同理,代入 ωnk+2n=−ωnk,得
F(ωnk+2n)=F1(ω2nk)−ωnkF2(ω2nk)
所以,我们可以根据 F1(ω2nk) 和 F2(ω2nk) 求出 F(ωnk) 和 F(ωnk+2n)。这种做法只能处理长度为 2 的正整次幂的多项式,所以我们要把高次系数补为 0。
接下来是 IDFT。它的操作与 DFT 极像,就是将 ωnk 变为 ωn−k,并在最后乘 n1。
现实计算中,递归处理效率较低,我们使用位逆序置换优化和蝶形运算优化,直接将值排列为特定的顺序,避免了递归和额外的临时数组。
位逆序置换与蝶形运算
我们要想避免递归,就要将需要一起计算的部分放在一起。
我们以 7 次多项式为例,有 8 个 ai,具体划分方式如图

规律很难注意到,我直接说了,就是将每个下标的二进制反转,以反转后的数为新下标。例如,3 的二进制是 011,反转后为 110,即 6,从图上来看,a3 确实到了原 a6 的位置。
位逆序置换后,我们可以直接计算 F1(x) 和 F2(x) 而无需临时数组,因为计算要用到的数与计算完成后的数应当被存在相同的下标内,直接覆盖原数就行了。具体地,计算 F(ωnk) 和 F(ωnk+2n) 时 F1(ω2nk) 的值在下标 k,F2(ω2nk) 的值在下标 k+2n。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #include<iostream> #include<cmath> #include<vector> using namespace std; constexpr int N=1<<22; const double PI=4*atan(1); struct Complex{ double real,imag; friend Complex operator+(const Complex &x,const Complex &y){ return {x.real+y.real,x.imag+y.imag}; } friend Complex operator-(const Complex &x,const Complex &y){ return {x.real-y.real,x.imag-y.imag}; } friend Complex operator*(const Complex &x,const Complex &y){ return {x.real*y.real-x.imag*y.imag,x.real*y.imag+y.real*x.imag}; } }; void DFT(vector<Complex> &f,int n,int rev){ for(int i=0,j=0;i<n;i++){ if(i<j) swap(f[i],f[j]); for(int k=n>>1;(j^=k)<k;k>>=1); } for(int k=2;k<=n;k<<=1){ double arg=2*PI*rev/k; Complex wn={cos(arg),sin(arg)}; for(int i=0;i<n;i+=k){ Complex w={1,0}; for(int j=0;j<k/2;j++){ Complex f1=f[i+j]; Complex f2=f[i+j+k/2]; f[i+j]=f1+w*f2; f[i+j+k/2]=f1-w*f2; w=w*wn; } } } if(rev==-1) for(int i=0;i<n;i++) f[i]={f[i].real/n,f[i].imag/n}; } int n,m,len; vector<Complex> a,b,c; int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=0,x;i<=n;i++){ cin>>x; a.push_back({double(x),0}); } for(int i=0,x;i<=m;i++){ cin>>x; b.push_back({double(x),0}); } len=1;while(len<=n+m) len<<=1; a.resize(len),b.resize(len),c.resize(len); DFT(a,len,1); DFT(b,len,1); for(int i=0;i<len;i++) c[i]=a[i]*b[i]; DFT(c,len,-1); for(int i=0;i<=n+m;i++) cout<<int(round(c[i].real))<<' '; return 0; }
|
代码实现的一些注意事项:
参考资料
https://www.cnblogs.com/Kenma/p/18813688
https://oi-wiki.org/math/poly/fft/
特别鸣谢,解决了我在数学上的一些疑惑。