快速傅里叶变换(后)
单位根
有了幅角和模长,怎么具体的把这个复数写出来呢?三角函数是个很有用的公式,高中知识告诉我们 表示当幅角为 , 模长为 时的 坐标, 表示 坐标。复平面中 和 分别表示 和 的值,所以我们就可以用三角函数将 次单位根表示出来,,同理 ,因为之前说过 次方就是将幅角乘 。我们还需要把公式换成弧度制,因为 C++ 的内置函数是弧度制的。弧度制很简单,只需要记住 即可。于是得到 ,这个式子叫做欧拉公式,好像也可以用泰勒展开得到,让人感叹数学的魔幻。
上图中, 为 值, 为 值。
因为 FFT 中需要正负成对出现的值,所以我们还得看看单位根的正负关系。复数 的相反数是 ,其几何意义是 坐标同时取反,也就是以原点为中心,旋转 。用向量表示就是模长相同,方向相反。这很重要,因为我们发现单位圆 等分,当 为偶数时,就能得到 对这样互为相反数的点,而它们都可以用单位根表示出来。
如上图,,, 对应的相反数就是 ,,。
所以我们可以得到当 为偶数时, (划重点)。
快速傅里叶变换
直接地说,快速傅里叶变换就是将单位根与其幂带入多项式求点值,并且利用单位根的性质让正负成对的性质一直保持下去,让分治一直成立,从而达到 的时间复杂度。
我们已经知道了当 为偶数时,,所以带入 得到 。翻译一下就是求出前 个根的 和 值后,后一半就直接知道了。这个分治是否能一直成立?会不会像实数一样递归一次后就不再是正负成对了?答案是不会,假如我们算的是正的一半(算负的一半也行):
,,,
它们的幅角分别为:
,,,(弧度制,)
带入 和 求点值要先平方,平方相当于幅角翻倍:
,,,
我们惊奇地发现这 个向量平方以后重新将圆等分为了 份,当然前提是 为偶数。如何理解?因为 为偶数,所以这前 个向量本身平分了 轴上方的半个单位圆。那么平方以后,幅角翻倍,就变成平分整个单位圆了。所以如果 还是偶数,就能继续分治下去,因为既然是平分单位圆,那就还是正负成对的。
因此,我们可以将多项式变成 的幂个项,多的项就当系数为 ,因为这样就能分治到底了,实现起来非常方便并且时间复杂度也是不变的。
例如多项式 应补成 ,因为 。
快速傅里叶逆变换
我们已经讲完了如何使用单位根使分治成立,从而在 内将系数表示法转换成点值表示法。但反过来怎么办呢?如何在 内将点值表示法转换成系数表示法?
点值的计算可以写成矩阵乘法的形式,矩阵乘法很简单,不会可以点这里。
其中最左边的矩阵是带入的单位根,每个项括号外有裹了个次方是因为多项式每项变量的次数是不一样的。 到 是多项式的系数。 到 是算出来的点值。
假如我们已经通过点值表示法的多项式乘法得到了结果多项式的点值,也就是说 到 是已知的。需要求结果多项式的系数,也就是说 到 是未知的。这里要用一些线性代数的知识。设 , 和 都是矩阵,且 (矩阵乘法记做 ),那么就有 ,其中矩阵 为矩阵 的逆矩阵。注意这里的 不是次方的意思,只是一种标记方法,表示逆矩阵。
所以我们只需要求出最左边的矩阵的逆矩阵就能通过乘以点值矩阵得到系数矩阵了。而这个工作数学家们已经帮我们完成了,涉及亿点线性代数,我就不展开了,想看完整证明的同学可以点这里。
求其逆矩阵最后的结果是:
上加一线表示共轭,也就是复数含 的项取反,前面的 表示矩阵内的每个项都除以 。总体来说,其逆矩阵就是每项取共轭,再除以 。
点值矩阵乘以这个逆矩阵得到的矩阵每项再除以 就是系数矩阵,但这个工作也是 的,如何加速?细心的读者可能已经发现了,这个过程和傅里叶快速变换是一样的,完全可以用相同的分治来加速,因为共轭实质上就是按 轴翻折,单位圆上单位根的性质不变。代码实现中两者只有一行语句的差别,也就是单位根含 项的正负号。
代码实现
#include <bits/stdc++.h>
#define N 5000000
#define err 1e-6
using namespace std;
int n, m;
const double pi = acos(-1.0);
complex<double> a[N], b[N];
void fft(complex<double> *a,int len,int inv){
if(len==1)return;
complex<double> e[len/2],o[len/2];
for(int i=0;i<len;i+=2)
e[i/2]=a[i],o[i/2]=a[i+1];
fft(e,len/2,inv); fft(o,len/2,inv);
complex<double> wn(cos(2*pi/len),sin(2*pi/len)*inv),wk(1,0);
for(int i=0;i<len/2;i++,wk*=wn)
a[i]=e[i]+wk*o[i],
a[i+len/2]=e[i]-wk*o[i];
}
int main(){
cin>>n>>m;
for(int i=0,buf;i<=n;i++)cin>>buf,a[i].real(buf);
for(int i=0,buf;i<=m;i++)cin>>buf,b[i].real(buf);
int len=1; while(len<n+m+1)len<<=1;
fft(a,len,1); fft(b,len,1);
for(int i=0;i<len;i++)a[i]*=b[i];
fft(a,len,-1);
for (int i=0;i<=n+m;++i)
printf("%.0f ",a[i].real()/len);
return 0;
}
参考资料
Fast Fourier Transform (2020), Youtube [online]. Available from: https://www.youtube.com/watch?v=h7apO7q16V0&feature=emb_logo [Accessed on June 1 2022]
一小时学会快速傅里叶变换 (2022), zhihu [online]. Available from: https://zhuanlan.zhihu.com/p/31584464 [Accessed on May 31 2022]
快速傅里叶变换 (2022), zhihu [online]. Available from: https://zhuanlan.zhihu.com/p/347091298 [Accessed on May 31 2022]
图片均来源于网络,侵权必删!