浅谈 FFT (快速傅里叶变换)
发布于 6 天前 15 次阅读
直接开始吧。
前置芝士
数论的话,学 NTT 之前再看也可以。
如果你都会就直接开始了。
引入:多项式乘法
七年级课本数学,应该不用怎么讲吧。
例如:
拆括号:
合并同类项:
上面的方法就相当于:
若 ,则
这样做复杂度是 的。
暴力代码:(模板题, )
#include<bits/stdc++.h> using namespace std; int n,m; int f[1000005],g[1000005],h[1000005]; int main(){ scanf("%d%d",&n,&m); for(int i=0;i<=n;i++)scanf("%d",&f[i]); for(int i=0;i<=m;i++)scanf("%d",&g[i]); for(int i=0;i<=n;i++) for(int j=0;j<=m;j++) h[i+j]+=f[i]*g[j]; for(int i=0;i<=n+m;i++)printf("%d ",h[i]); return 0; }
点值转化
考虑优化这个暴力。
我们想,如果我们的乘法只要 ,那就很妙了。
根据拉格朗日插值,我们知道,在平面直角坐标系中, 个点就可以确定一个 次多项式。
用 个点来表示一个 次多项式的方法,就是点值表示。
我们就会想到,能否将一个 次多项式转化为 个点呢?那么两个多项式相乘的点值表示,不就是这 个点值的相乘吗?
例如,我们有两个多项式 和 。
那么 (确定了 这个多项式)
(确定了 )
我们设 。
那么很显然, 。
好像有点不对啊, 的次数应该是 ,需要 个点啊。
那我们再找两个点不就好了……
好了现在我们知道 。
根据拉格朗日插值公式,
嗯,就是这样。
对于两个次数为 的多项式相乘,我们有一种策略:分别代入 个数,求出在这两个多项式上的点值,然后把这些点值相乘,最后再把点值转化为多项式。
我们把第一步——求点值叫做 DFT ,第二步——求多项式叫做 IDFT 。综合起来,就叫做 FFT 。
一个显而易见的事实是, IDFT 是 DFT 的CP逆运算。
有关复数
不会吧不会吧不会真的有人还没学会复数吧
一个复数的形式是 ,这里 是 的平方根之一(另一个是 ,虽然他们两个哪个都不大于 (也不小于 (因为复数之间好像不能作大小比较来着)))。
复数的运算很简单,就跟多项式差不多(你可以认为 是一个未知数)。
而这些运算在几何上也有一些神奇的性质:
假设我们有一个复数 :
我们让它对应平面直角坐标系上的点 。
定义一个复数的模长为 ,也就是它对应的点到原点 的距离。
定义它的幅角为它对应的点到原点的这条线段与 轴的夹角(带正负的角,如果不会自行百度),用式子就是 。
然后,我们看两个复数 和 。
它们的乘积是 。
我们把模长代进去:
把幅角的 值代进去:
所以我们得到:复数相乘时,模长相乘,幅角相加。
单位根
学过三角函数的,一定都知道单位圆。
我们把单位圆搬到复平面上,那么在单位圆上的复数,模长一定为 。
接下来讨论的复数,均是单位圆上的复数,也即模长为 的复数。
我们像切蛋糕一样,从 开始,把这个圆切成 块,切出的点就是 。
拿张图过来:
图中标出了 。
显然, 次单位根是 的复数根。
因此 。
同时,我们由图容易知道, (或者更一般地,上下标可以同时乘除任意一个正整数虽然没用)。
DFT 过程
我们把单位根代进多项式 !
我们把 拆成奇次项和偶次项,提出一个 : ,设
这里保证 是 的幂,不会分得不均匀。
我们发现, 和 都是关于 的多项式。
现在我们把单位根代进去。
当 时,代入 :
代入 :
哈!这两个等式不就只有一个正负号的区别吗?
辣么我们就可以用分治的思路,每次把 减半,当然,到 时,我们代入的只有 ,此时什么也不要做。
IDFT 过程
上面说过, IDFT 是 DFT 的CP逆运算,它可以把点值表示转成系数表示。
其实它们就只有一句话的区别:只要把 DFT 中的 全部换成 ,做完之后除以 即可。
证明只需证(使用矩阵求逆的证法):
显然
我们假设
那么
验证:
所以 确实是单位矩阵。
递归实现 FFT
#include<bits/stdc++.h> using namespace std; const double pi=acos(-1.0); struct CP{ double x,y; CP(double xx=0,double yy=0){ x=xx,y=yy; } CP operator +(CP const &b)const{ return CP(x+b.x,y+b.y); } CP operator -(CP const &b)const{ return CP(x-b.x,y-b.y); } CP operator *(CP const &b)const{ return CP(x*b.x-y*b.y,x*b.y+y*b.x); } }f[2097160],g[2097160],sav[2097160]; int n,m; void fft(CP *f,int len,bool flag){ if(len==1)return; for(int k=0;k<len;k++)sav[k]=f[k]; for(int k=0;k<len/2;k++)f[k]=sav[k<<1],f[k+len/2]=sav[k<<1|1]; fft(f,len/2,flag); fft(f+len/2,len/2,flag); CP Ur(cos(2*pi/len),sin(2*pi/len)); if(!flag)Ur.y*=-1; CP now(1,0); for(int k=0;k<len/2;k++){ sav[k]=f[k]+now*f[k+len/2]; sav[k+len/2]=f[k]-now*f[k+len/2]; now=now*Ur; } for(int k=0;k<len;k++)f[k]=sav[k]; } int main(){ scanf("%d%d",&n,&m); for(int i=0;i<=n;i++)scanf("%lf",&f[i].x); for(int i=0;i<=m;i++)scanf("%lf",&g[i].x); m+=n; n=1; while(n<=m)n<<=1; fft(f,n,1);fft(g,n,1); for(int i=0;i<n;i++)f[i]=f[i]*g[i]; fft(f,n,0); for(int i=0;i<=m;i++)printf("%d ",(int)(f[i].x/n+0.499)); return 0; }
这样已经足以通过板子了,但众所周知递归非常慢,所以我们要卡卡常(
迭代实现 FFT
显然,递归可以直接改成迭代(手动从最后一层开始做)。可是我们没法知道哪个位置最后对应到哪个位置。
然后,我们发现:每个位置,经过原来的递归后,必定会到另一个位置等于没说。
关键在于:
哎!用二进制来表示,最后对应的位置就是原来位置的二进制反过来!
然后我们就可以递推每个数反过来是什么样子,然后就能做了。
#include<bits/stdc++.h> using namespace std; const double pi=acos(-1.0); struct CP{ CP (double xx=0,double yy=0){x=xx,y=yy;} double x,y; CP operator +(CP const &B)const{ return CP(x+B.x,y+B.y); } CP operator -(CP const &B)const{ return CP(x-B.x,y-B.y); } CP operator *(CP const &B)const{ return CP(x*B.x-y*B.y,x*B.y+y*B.x); } }f[2097160],g[2097160]; int n,m; int tr[2097160]; void fft(CP *f,int n,int flag){ for(int i=0;i<n;i++)if(i<tr[i])swap(f[i],f[tr[i]]); for(int p=2;p<=n;p<<=1){ int len=p>>1; CP Ur(cos(2*pi/p),sin(2*pi/p)); if(!flag)Ur.y*=-1; for(int k=0;k<n;k+=p){ CP now(1,0); for(int i=k;i<=k+len-1;i++){ CP num=now*f[i+len]; f[i+len]=f[i]-num; f[i]=f[i]+num; now=now*Ur; } } } } int main(){ scanf("%d%d",&n,&m); for(int i=0;i<=n;i++)scanf("%lf",&f[i].x); for(int i=0;i<=m;i++)scanf("%lf",&g[i].x); m+=n; n=1; while(n<=m)n<<=1; for(int i=0;i<n;i++)tr[i]=(tr[i>>1]>>1)|((i&1)?(n>>1):0); fft(f,n,1);fft(g,n,1); for(int i=0;i<n;i++)f[i]=f[i]*g[i]; fft(f,n,0); for(int i=0;i<=m;i++)printf("%d ",(int)(f[i].x/n+0.499)); return 0; }
</main>