最近舟游疯狂出货,心情很好~
具体的推导见这篇:胡小兔 - 小学生都能看懂的FFT!!! (写的很好,不过本小学生第一次没看懂0.0)
总结下关键内容
~ Part 1 ~ 多项式表示转点值表示:
将原多项式$A(x)$的次数补成$2$的幂次,然后进行递归拆分
设当前一层的多项式为$B(x)$,项数为$n$,即:$B(x)=b_0+b_{1}x+b_{2}x^{2}+...+b_{n-1}x^{n-1}$
先将此多项式的按照奇偶项系数拆成两个式子
\[B_0(x)=b_0+b_2x+b_4x^2+...+b_{n-2}x^{\frac{n}{2}-1}\]
\[B_1(x)=b_1+b_3x+b_5x^2+...+b_{n-1}x^{\frac{n}{2}-1}\]
那么可以将$B(x)$重新表示:$B(x)=B_0(x^2)+xB_1(x^2)$
我们希望将复平面上单位圆的点代入,得到$B(\omega^{1}_{n})$,...,$B(\omega^{n}_{n})$的值
若$0\leq i<\frac{n}{2}$,则有【这里使用了复数运算律:因为$\omega$在单位圆上,所以$\omega^i_n\cdot\omega^j_n=\omega^{i+j}_n$;因为$n$为$2$的倍数,所以$\omega^i_n=\omega^{\frac{i}{2}}_{\frac{n}{2}}$】
\begin{align*}B(\omega^i_n)&=B_0((\omega^i_n)^2)+\omega^i_nB_1((\omega^i_n)^2)\\&=B_0(\omega^{2i}_n)+\omega^i_nB_1(\omega^{2i}_n)\\&=B_0(\omega^i_{\frac{n}{2}})+\omega^i_nB_0(\omega^i_{\frac{n}{2}})\end{align*}
而对于单位圆上的另一半,则有【这里追加一个复数运算律:因为$n$为$2$的倍数,所以$\omega^{i+\frac{n}{2}}_n=-\omega^i_n$】
\begin{align*}B(\omega^{i+\frac{n}{2}}_n)&=B_0((\omega^{i+\frac{n}{2}}_n)^2)+\omega^{i+\frac{n}{2}}_nB_1((\omega^{i+\frac{n}{2}}_n)^2)\\&=B_0(\omega^{2i+n}_n)-\omega^i_nB_1(\omega^{2i+n}_n)\\&=B_0(\omega^i_{\frac{n}{2}})-\omega^i_nB_0(\omega^i_{\frac{n}{2}})\end{align*}
所以我们将这一层计算$B(\omega^0_n)$,...,$B(\omega^{n-1}_{n-1})$的问题变为计算下一层$B_0(\omega^0_{\frac{n}{2}})$,...,$B_0(\omega^{\frac{n}{2}-1}_{\frac{n}{2}})$,$B_1(\omega^0_{\frac{n}{2}})$,...,$B_1(\omega^{\frac{n}{2}-1}_{\frac{n}{2}})$
这样,总的复杂度是$O(N logN)$
~ Part 2 ~ 点值转多项式系数
设原多项式为$A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}$,其中补成$2$的幂次项后项数为$n$
则分别代入$\omega^0_n$,$\omega^1_n$,...,$\omega^{n-1}_n$,得到$y_i=A(\omega^i_n)$,$i=0,1,...,n-1$,即上面得到的$n$个点值
令$B(x)=y_0+y_1x+y_2x^2+...+y_{n-1}x^{n-1}$
再分别代入$\omega^0_n$,$\omega^{-1}_n$,...,$\omega^{-(n-1)}_n$,运算(这里略去)得到$a_i=\frac{B(\omega^{-i}_n)}{n}$,$i=0,1,...,n-1$
按这个思路写出的代码如下(实现了多项式系数转点值,再转回多项式系数)
大佬用的数组指针确实方便
#include <cstdio> #include <complex> #include <cmath> #include <cstring> #include <algorithm> using namespace std; typedef complex<double> cp; const int N=100005; const double pi=acos(-1.0); inline cp omega(int x,int n,int rev) { if(rev) x=-x; return cp(cos(x*2*pi/(double)n),sin(x*2*pi/(double)n)); } cp tmp[N]; void fft(cp *a,int n,int rev) { if(n==1) return; for(int i=0;i<n;i++) tmp[i]=a[i]; for(int i=0;i<n/2;i++) { a[i]=tmp[i*2]; a[n/2+i]=tmp[i*2+1]; } fft(a,n/2,rev); fft(a+n/2,n/2,rev); for(int i=0;i<n/2;i++) { cp x=omega(i,n,rev); tmp[i]=a[i]+x*a[n/2+i]; tmp[n/2+i]=a[i]-x*a[n/2+i]; } for(int i=0;i<n;i++) a[i]=tmp[i]; } int n; cp a[N]; int main() { // freopen("input.txt","r",stdin); scanf("%d",&n); for(int i=0;i<n;i++) { int x; scanf("%d",&x); a[i]=cp((double)x,0); } int sz=1; while(sz<n) sz<<=1; n=sz; fft(a,n,0); fft(a,n,1); for(int i=0;i<n;i++) printf("%.2lf ",a[i].real()/(double)n); printf("\n"); return 0; }
~ Part 3 ~ 两个简单的优化
非递归:
将递归的第一部分,即交换$a[i]$以奇偶分组,直接完成
这里的交换位置是两两一对的——若$n=8$,则$4$($100$)最终将与$1$($001$)互换
即两个交换位置的二进制是前后翻转的
去除$tmp[i]$数组:
对于递归的第二部分for循环中的每个$i$,只会用到$tmp[i]$、$tmp[n/2+i]$、$a[i]$、$a[n/2+i]$
所以对于当前$i$的改变是无后效性的,可以考虑使用临时变量而不是$tmp[i]$数组
于是,可以写出没有递归与$tmp[i]$数组的$fft()$函数
void fft(cp *a,int n,int rev) { for(int i=0;i<n;i++) { int nxt=0; for(int j=1;j<=n;j<<=1) if(i&j) nxt+=n/2/j; if(nxt>i) swap(a[i],a[nxt]); } for(int i=2;i<=n;i<<=1) for(int j=0;j<n;j+=i) for(int k=0;k<i/2;k++) { cp x=omega(k,i,rev); cp b0=a[j+k],b1=a[i/2+j+k]; a[j+k]=b0+x*b1; a[i/2+j+k]=b0-x*b1; } }
用FFT计算卷积
(待续)