看了半天的FFT,总算是看懂了?
FFT(快速傅里叶变换)是用来解决多项式乘法的算法,时间复杂度为(O(nlog_2n))。
多项式的系数表示法和点值表示法
如何可以准确地表达一个(n)次的多项式?一种方法是系数表示法,直接给出(n+1)项的系数;另一种方法是点值表示法,给出(n+1)个点,用这些点唯一确定一个多项式。
在进行多项式乘法的时候,如果是用系数表示法,似乎不可避免地要用(O(n^2))的时间复杂度来获得答案;但是如果使用点值表示法,如果各个点的横坐标取值都相同,就可以(O(n))实现多项式乘法(只要把纵坐标相乘就可以得到所要的多项式)。
可以看出,用点值表示法可以很快地计算两个多项式的乘积,但是问题又来了,一般情况下,我们都是用系数表示法来表示一个多项式,系数表示法和点值表示法的转化似乎也是要(O(n^2))((O(n^3))?),有没有一种方法可以很快地实现两种表示方法的转化呢?
复数的引入
在实数中,并不存在(sqrt{-1}),扩展到复数集(C)后我们定义(i^2=-1),那么(i=sqrt{-1}),一个复数(z)可以表示为(a+bi),其中(a.bin R)。
可以用平面直角坐标系的一个点((x,y))表示一个复数(z=x+yi),准确来说,这个平面直角坐标系应该就是复平面直角坐标系。当然我们可以用另一种方法表示平面直角坐标系上的一个点:((r, heta)),表示该点距原点的距离为(r),其与原点连线和(x)轴正方向夹角为( heta)。同样的,我们也可以用这种方法表示一个复数。
复数的运算类似实数,把(i)当成一个普通的数就好了。
复数相乘的一个小性质:
(不会证呜呜呜,其实如果知道了三角函数的一些公式很容易证明,可以参考this)
FFT快速傅里叶变化
DFT(离散傅里叶变换)就直接跳了吧。
先假设(n)为2的整数次幂。
回到之前的问题,如何快速地在系数表示法和点值表示法之间进行转换。
如果我们取(x_0,x_1,dots,x_{n-1})这些数作为横坐标的话,多项式为(f(x)=a_0x^0+a_1x^1+a_2x^2+dots+a_{n-1}x^{n-1}),那么有:
FFT相当于就是要快速求出这个的值,如果第一个矩阵有逆矩阵的话,也可以通过这个式子将点值表示法转化为系数表示法。
接下来看如何选取(x)的值来减小计算量,可以选取在复平面直角坐标系中单位圆上的点(没学过复数,可能表述不准确,就是那个意思),把在复平面直角坐标系中单位圆平均分成(n)等份。
借的一张图:
设(w_n)为图中的1号点,那么这(n)个等分点就分别是(w_n^0,w_n^1,dots,w_n^{n-1}),根据之前复数乘法的性质,可以得到:
(w_n)称作(n)次单位根,把这些点作为横坐标的话,可以减小计算量。
先看看单位根的几个性质吧:
(w_n^0=w_n^n=1,w_n^{n/2}=-1,w_n^k=w_{2n}^{2k},w_n^k=w_n^{kmod n})
直接带入公式就可以得到了。
此时我们只需要求(w_n)的0到(n-1)次幂就可以求出所有(x)的幂的值了,减小了许多计算量。
然而复杂度还是(O(n^2))。
怎么办呢,考虑分治,原多项式为:
设:
(f_1(x)=a_0x^0+a_2x^1+a_4x^2+cdots +a_{n-2}x^frac{n-2}{2})
(f_2(x)=a_1x^0+a_3x^1+a_5x^2+cdots +a_{n-1}x^frac{n-2}{2})
不难发现(f(x)=f_1(x^2)+xcdot f_2(x^2))
将(w_n^k)带入,得到(f(w_n^k)=f_1(w_n^{2k})+w_n^kcdot f_2(w_n^{2k}))
又:(w_{n}^{k}=w_{n/2}^{k})
看到这个方程,应该是可以直接推出递归是怎么写的了,首先拆系数,然后递归处理两组系数的解,得到(f_1(w_{n/2}^{0dots n/2-1}))和(f_2(w_{n/2}^{0dots n/2-1})),根据这两组的解得出(f(w_n^{0dots n-1}))。当递归到(n=1)时,只需要求(f(w_1^0)=a_0(w_1^0)^0=a_0),解就是唯一的系数。
有没有可以优化计算的地方呢?实际上是有的:
可以发现,它和(f(w_n^k))好像差别不大,所以可以在算(f(w_n^k))的时候顺便把它也给算了(好像没区别?(雾)。
然后还有一个特殊的地方,就是最后求出来的(y)值可以存在和系数相同的数组里面,然后可以节省空间复杂度。
但是别忘了还有一个问题,就是FFT的逆运算IFFT,那么如果把单位根作为(x),那个矩阵有逆矩阵吗?
相当于接下来我们是要对下面这个矩阵求逆:
矩阵求逆后是这样的(干脆记住这个结论算了):
代码大概是这样的:
void fft(complex*a,int n,int inv){
if(n==1)return;
for(int i=0;i<n;i+=2){
tmp0[i/2]=a[i];
tmp1[i/2]=a[i+1];
}
for(int i=0;i<n/2;++i)
a[i]=tmp0[i],a[i+n/2]=tmp1[i];
fft(a,n/2);fft(a+n/2,n/2);
const complex BASE(cos(pi*2/n),inv*sin(pi*2/n));
complex w(1,0);
for(int i=0;i<n/2;++i){
complex fir=a[i],sec=a[i+n/2]*w;
a[i]=fir+sec,a[i+n/2]=fir-sec;w=w*BASE;
}
}
然后连板子都过不去(没有亲测,听说的)。
复杂度明明是对的啊,但是递归就是慢,还有爆栈的风险,怎么办?写非递归板的,模拟一遍得出系数(a_i)最终所在的位置,直接枚举合并长度,一层层地合并就可以了。
不想打模拟?有一个结论:(i)变换后的最终位置为({i ext{的二进制倒置}})。
如何证明?从低位往高位考虑的话,如果是0,就往左走,如果是1,就往右走,然后从下层往上层考虑,如果是1,贡献为2的当前经过层数次幂,如果是0,贡献为0;仔细想想,不就是二进制的倒置吗?所以可以先把每个数的最终位置算出来。
如何求二进制倒置?如下:
for(int i=1;i<=n;++i)
rev[i]=(rev[i>>1]>>1)|((i&1)<<maxbit);
然后就可以愉快地切模板了。
void fft(complex*a,int n,int inv){
for(int i=0;i<n;++i)
if(i<rev[i])swap(a[i],a[rev[i]]);
for(int len=2;len<=n;len<<=1){
int mid=len>>1;
const complex BASE(cos(pi/mid),sin(pi/mid)*inv);
for(int i=0;i<n;i+=len){
cpx w(1,0);
for(int j=0;j<mid;++j,w*=BASE){
cpx fir=a[i+j],sec=a[i+j+mid]*w;
a[i+j]=fir+sec,a[i+j+mid]=fir-sec;
}
}
}
}