基本概念
定义
记一个关于 x 的式子:
f(x)=i=0∑nai×xi
为一个 n 次多项式。其中 ai 为常数,n 为该多项式的度,也称次数,记作 degf。
显然,f(x) 可以看作一个关于 x 的 n 次函数 y=f(x)。
根据我们的初中常识,n+1 个点的坐标可以唯一确定一个 n 次函数的解析式,进而可以确定这个多项式,这种方式被称为多项式的点值表示法.
对应的,写成 f(x)=i=0∑nai×xi 形式被称为多项式的系数表示法.
多项式加减法
设 n 次多项式 A(x)=∑i=0nai×xi,B(x)=∑i=0nbi×xi,则:
f(x)=A(x)±B(x)=i=0∑n(ai±bi)xi
多项式乘法
f(x)=A(x)×B(x)=i=0∑nj=0∑nai×bj×xi+j
使用点值表示法时,直接将其横坐标对应的纵坐标进行加减乘即可,不过做乘法时,需要 A(x) 和 B(x) 各给出 2n 组点值。
快速傅里叶变换(FFT)
前置知识:复数、单位根
FFT 能够支持在 O(nlogn) 的时间内计算两个度为 n 的多项式的乘法。
我们知道系数表示法需要 O(n2) 的时间复杂度,而点值表示法只需要 O(n) ,因为只需要将对应点值的纵坐标相乘就可以了。因此我们考虑将一个多项式从系数表示法改为点值表示法,相乘后再改回系数表示法。如果这两个操作的时间复杂度能够低于 O(n2) ,那么就可以得到一个比暴力更优的做法。
而求出 n 次多项式在每个 n 次单位根下的点值的过程叫做离散傅里叶变换(DFT),将这些点值重新插值为系数表示法的过程叫做离散傅里叶逆变换(IDFT)。
以下设进行变换的多项式为 (n−1) 次多项式 A(x)=∑i=0n−1ai×xi。其中 n 是 2 的整数幂。如果不是的话,则向 A(x) 的更高次数位 n 补充 an=0 ,令其成为 n 次多项式,一直进行直到其次数加一的值是 2 的整数幂,取 n 等于其次数,m=2n。
DFT
考虑求出一个长度为 n 的数列 {bi},这个数列的第 k 项为 A(x) 在 n 次单位根的 k 次幂处的点值。
因此有
bk = i=0∑n−1ai×ωni
这个过程是 O(n2) 的,可以使用 FFT 来优化。
FFT
我们对 A(x) 按照系数的编号的奇偶性进行分类:
A(x) = i=0∑n−1aixi = i=0∑m−1a2i×x2i+i=0∑m−1a2i+1×x2i+1= i=0∑m−1a2ixi2+xi=0∑m−1a2i+1x2i = i=0∑m−1a2i(x2)i+xi=0∑m−1a2i+1(x2)i
设 A0(x) 是一个 (m−1) 次多项式,满足
A0 = i=0∑m−1a2ixi
设 A1(x) 是一个 (m−1) 次多项式,满足
A1 = i=0∑m−1a2i+1xi
联立以上三式,可以得到
A(x) = A0(x2)+x×A1(x2)
接着,参考单位根的性质:
(ωnk)2=ωmkωnk+m=−ωnk
我们可以分开考虑小于 m 的点值和大于 m 的点值。
对于 0≤k<m,有:
A(ωnk) = A0((ωnk)2)+wnkA1((ωnk)2)= A0(ωmk)+wnkA1(ωmk)
再考虑大于 m 次的点值:
A(ωnm+k) = A0((ωnm+k)2)+ωnm+kA1((ωnm+k)2)= A0((wnk)2)+−ωnkA1((ωnk)2)= A0(ωmk)−wnkA1(ωmk)
可以发现,只要求出 A0 和 A1 小于 m 次的点值,就可以线性求出 A 在整个 n 次幂处的点值,而 A0 和 A1 在小于 m 次的点值是可以递归求解的。时间复杂度 O(nlogn)。
IDFT
至此我们得到了 bk = ∑i=0n−1ai×ωni,而现在希望还原系数数列 {ai}。
这里直接给出结论,推导和证明请参考其它文章:
ak = n1i=0∑n−1biωn−ki
仍然要尝试优化,那就是 IFFT,即快速傅里叶逆变换。
IFFT
由于 wn−k 可以看做 n 次本原单位根每次逆时针旋转本原单位根幅角的弧度,因此 ωn−k 和 ωnk 是一一对应的。具体的,wn−k=wnk+n。因此我们只需要使用 FFT
的方法,求出 B(x) 在 ωn 各个幂次下的值,然后数组反过来,即令 ak = n1∑i=0nB(wnn−k) 即可。
时间复杂度 O(nlogn)。
代码
以下代码用迭代法实现: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 66 67
| #include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=6e6+5; const double pi=acos(-1); int n,m,sm,k; complex<double> f[maxn],g[maxn]; int tax[maxn]; inline void makerev(int x) { int d=x>>1,num=0; tax[num++]=0,tax[num++]=d; for(int w=2;w<=x;w<<=1) { d>>=1; for(int p=0;p<w;p++) tax[num++]=tax[p]|d; } } inline void fft(complex<double> *a,int x) { for(int i=1;i<x;i++) if(tax[i]>i) swap(a[i],a[tax[i]]); for(int len=2,mid=1;len<=x;mid=len,len<<=1) { complex<double> add(cos(pi/mid),sin(pi/mid)),init(1.0,0.0); for(int l=0,r=len-1;r<=x;l+=len,r+=len) { auto w=init; for(int p=l;p<l+mid;p++) { auto x=a[p]+w*a[p+mid]; auto y=a[p]-w*a[p+mid]; a[p]=x,a[p+mid]=y; w*=add; } } } } int main() { ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=0;i<=n;i++) { int tmp; cin>>tmp; f[i]=tmp; } for(int i=0;i<=m;i++) { int tmp; cin>>tmp; g[i]=tmp; } sm=n+m,k=1; while(k<=sm) k<<=1; makerev(k); fft(f,k); fft(g,k); for(int i=0;i<k;i++) f[i]*=g[i]; fft(f,k); reverse(f+1,f+k); for(int i=0;i<=sm;i++) cout<<int(f[i].real()/k+0.5)<<" "; return 0; }
|
关于如何递归转迭代,先挖个坑,以后再补。
未完待续……