快速傅里叶变换
快速傅里叶变换
update:2020.7.29 增加了三次变两次优化
【预告】
- 代码奇短
- 大量公式推导
【前言】
在解决多项式卷积(乘法)的问题时,直接相乘的
这时,我们就需要寻找其它的路径来计算卷积。
众所周知,二维坐标系上横坐标互异的
与用各项系数表示多项式的系数表示法不同,用点表示一个多项式的方法叫点值表示法。
离散傅里叶变换(
相对地,把一个多项式的点值表示法转化为系数表示法的过程,就是离散逆傅里叶变换(
快速傅里叶变换(
【前置知识】
-
三角函数(不讲)
-
复数运算(不讲)
-
复平面及单位根(不想讲,百度有)
【数学推导部分】
首先,我们来看看单位根的性质
-
\omega_n^k=cos\frac{2k\pi}{n}+isin\frac{2k\pi}{n} -
\omega_{2n}^{2k}=\omega_n^k -
\omega_{n}^{k+n}=cos\frac{2(k+n)\pi}{n}+isin\frac{2(k+n)\pi}{n}=cos(\frac{2k\pi}{n}+2\pi)+isin(\frac{2k\pi}{n}+2\pi) =cos\frac{2k\pi}{n}+isin\frac{2k\pi}{n}=\omega_n^k -
\omega_{n}^{k+\frac{n}{2}}=cos\frac{2(k+\frac{n}{2})\pi}{n}+isin\frac{2(k+\frac{n}{2})\pi}{n}=cos(\frac{2k\pi}{n}+\pi)+isin(\frac{2k\pi}{n}+\pi) =-cos\frac{2k\pi}{n}-isin\frac{2k\pi}{n}=-\omega_n^k
再看看这个多项式
为了方便讨论,以下提及的多项式的长度
我们在DFT中,其实要计算的的是
-
F(x_0)=a_{0}+a_1x_0+a_2x_0^2+...+a_{n-2}x_0^{n-2}+a_{n-1}x_0^{n-1} F(x_1)=a_{0}+a_1x_1+a_2x_1^2+...+a_{n-2}x_1^{n-2}+a_{n-1}x_1^{n-1} F(x_2)=a_{0}+a_1x_2+a_2x_2^2+...+a_{n-2}x_2^{n-2}+a_{n-1}x_2^{n-1} ... ...
F(x_{n-1})=a_{0}+a_1x_{n-1}+a_2x_{n-1}^2+...+a_{n-2}x_{n-1}^{n-2}+a_{n-1}x_{n-1}^{n-1}
观察每一个
-
F(x)=a_{0}+a_1x+a_2x^2+...+a_{n-2}x^{n-2}+a_{n-1}x^{n-1}
按次数奇偶性分类,设
-
G(x)=a_0+a_2x+a_4x^2+...+a_{n-4}x^{\frac{n}{2}-2}+a_{n-2}x^{\frac{n}{2}-1} -
H(x)=a_1+a_3x+a_5x^2+...+a_{n-3}x^{\frac{n}{2}-2}+a_{n-1}x^{\frac{n}{2}-1}
可以得到
-
F(x)=a_0+a_2x^2+...+a_{n-2}x^{n-2}+a_1x+a_3x^3...+a_{n-1}x^{n-1} =H(x^2)+xG(x^2)
不妨把单位根
-
F(\omega_n^k)=H(\omega_n^{2k})+\omega_n^kG(\omega_n^{2k}) =H(\omega_{\frac{n}{2}}^k)+\omega_n^kG(\omega_{\frac{n}{2}}^k)
再代一个
-
F(\omega_n^{k+\frac{n}{2}})=H(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}G(\omega_n^{2k+n})=H(\omega_n^{2k})-\omega_n^kG(\omega_n^{2k}) =H(\omega_{\frac{n}{2}}^k)-\omega_n^kG(\omega_{\frac{n}{2}}^k)
还没发现问题?我把要求的式子再列出来
-
F(\omega_n^0)=H(\omega_\frac{n}{2}^0)+\omega_n^0G(\omega_\frac{n}{2}^0) -
F(\omega_n^1)=H(\omega_\frac{n}{2}^1)+\omega_n^1G(\omega_\frac{n}{2}^1) ... ...
-
F(\omega_n^{\frac{n}{2}-1})=H(\omega_{\frac{n}{2}}^{\frac{n}{2}-1})+\omega_n^{\frac{n}{2}-1}G(\omega_{\frac{n}{2}}^{\frac{n}{2}-1})
-
F(\omega_n^\frac{n}{2})=H(\omega_\frac{n}{2}^0)-\omega_n^0G(\omega_\frac{n}{2}^0) -
F(\omega_n^{\frac{n}{2}+1})=H(\omega_\frac{n}{2}^1)-\omega_n^1G(\omega_\frac{n}{2}^1) ... ...
-
F(\omega_n^{n-1})=H(\omega_{\frac{n}{2}}^{n-1})-\omega_n^{n-1}G(\omega_{\frac{n}{2}}^{n-1})
计算一半就能知道另一半的结果
原来
如果用递归来完成每一步这样的操作,将
递归式可以写出来。经过若干次分组之后,当前计算的多项式的长度变为
每层递归中只需要写成下面这样
其中
如下图,以
不废话了,先来看看代码
顺便给出一个复数数据类型模板
const double pi=acos(-1.0);
struct Cplx{
double Re,Im;
Cplx(double p=0,double q=0):Re(p),Im(q){}
Cplx operator + (const Cplx &a) const {return Cplx(Re+a.Re,Im+a.Im);}
Cplx operator - (const Cplx &a) const {return Cplx(Re-a.Re,Im-a.Im);}
Cplx operator * (const Cplx &a) const {return Cplx(Re*a.Re-Im*a.Im,Re*a.Im+Im*a.Re);}
}A[MAXN],B[MAXN],C[MAXN];
void FFT(Cplx *a,int n){
if(n==1) return;
int m=n>>1;Cplx a1[m],a2[m];
for(int i=0;i<m;i++){//分类
a1[i]=a[i<<1];
a2[i]=a[i<<1|1];
}
FFT(a1,m);FFT(a2,m);//递归
Cplx w1=Cplx(cos(2.0*pi/n),sin(2.0*pi/n)),wk=Cplx(1,0);
for(int i=0;i<m;i++,wk=wk*w1){//计算
a[i]=a1[i]+wk*a2[i];
a[i+m]=a1[i]-wk*a2[i];
}
return;
}
讲了这么久,终于讲完了
经过一波神奇的操作,
以下是公式推导时间
刚刚我们利用系数
相当于计算了序列
再尝试一下对
设一条序列
那么
我们先看看这条式子
当
当
所以
设对于序列的运算
有
至此,DFT和IDFT我们都找到了办法来实现
【递归实现代码】
先上一个AC不了模板题的递归代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
const double pi=acos(-1.0);
const int MAXN=5000005;
struct Cplx{
double Re,Im;
Cplx(double p=0,double q=0):Re(p),Im(q){}
Cplx operator + (const Cplx &a) const {return Cplx(Re+a.Re,Im+a.Im);}
Cplx operator - (const Cplx &a) const {return Cplx(Re-a.Re,Im-a.Im);}
Cplx operator * (const Cplx &a) const {return Cplx(Re*a.Re-Im*a.Im,Re*a.Im+Im*a.Re);}
}A[MAXN],B[MAXN],C[MAXN];
int N,M,L;
void FFT(Cplx *a,int n,int d){
if(n==1) return;
int m=n>>1;Cplx a1[m],a2[m];
for(int i=0;i<m;i++){
a1[i]=a[i<<1];
a2[i]=a[i<<1|1];
}
FFT(a1,m,d);FFT(a2,m,d);
Cplx w1=Cplx(cos(2.0*pi/n),d*sin(2.0*pi/n)),wk=Cplx(1,0);
for(int i=0;i<m;i++,wk=wk*w1){
a[i]=a1[i]+wk*a2[i];
a[i+m]=a1[i]-wk*a2[i];
}
return;
}
void Input(){
scanf("%d%d",&N,&M);
for(int i=0;i<=N;i++) scanf("%lf",&A[i].Re);
for(int i=0;i<=M;i++) scanf("%lf",&B[i].Re);
for(L=1;L<N+M+1;L<<=1);
return;
}
int main(){
Input();
FFT(A,L,1);
FFT(B,L,1);
for(int i=0;i<=L;i++)
C[i]=A[i]*B[i];
FFT(C,L,-1);
for(int i=0;i<=N+M;i++)
printf("%d ",(int)(C[i].Re/L+0.5));
return 0;
}
【迭代优化】
复杂度没错,是
我们需要一点小小的优化
再回来看看这个图
我们发现
-
可以先把多项式的系数排成图中最下面一层的顺序,再自底向上运算
-
计算时只会用到向下一层的数,完全可以只用同一个数组覆盖着计算
关于第一点,我们发现有一种神奇的方法可以
还是以
最底层是
恰好是
像这样
根据这个原理,我们可以计算出
先看一条式子,也许你就会
没错!这样算就是
只要反过来
就行了。不信自己推
并且在将系数排成
for(int i=0;i<=n-1;i++)
if(rnk[i]>i) swap(a[i],a[rnk[i]]);
就可以了
只要我们第一层循环枚举当前要合并的区间长度
第二层枚举完每一个要合并的区间,其中
再像递归的做法那样在
令
详情看代码
for(int n=2;n<=L;n<<=1){
Cplx w1(cos(2.0*pi/n),d*sin(2.0*pi/n));
for(int i=0;i<L;i+=n){
Cplx wk(1,0);
for(int j=i;j<i+(n>>1);j++,wk=wk*w1){
Cplx t1=a[j],t2=a[j+(n>>1)]*wk;
a[j]=t1+t2;
a[j+(n>>1)]=t1-t2;
}
}
}
FFT特色:三次变两次优化
将B放到A的虚部上,得到
用FFT计算它的平方,得
结果的虚部/2就是答案了。别忘了除以L。
这样可以少做一次FFT,常数小。
【模板代码】Luogu P3803
注意一下细节就能AC了
//Fast Fourier Transformation
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
const int MAXN=4000005;
const double pi=acos(-1.0);
struct Cplx{
double Re,Im;
Cplx(double re=0,double im=0):Re(re),Im(im){}
Cplx operator + (const Cplx &a) const {return Cplx(Re+a.Re,Im+a.Im);}
Cplx operator - (const Cplx &a) const {return Cplx(Re-a.Re,Im-a.Im);}
Cplx operator * (const Cplx &a) const {return Cplx(Re*a.Re-Im*a.Im,Re*a.Im+Im*a.Re);}
}A[MAXN],B[MAXN],C[MAXN];
int N,M,L,rnk[MAXN];
void Input(){
scanf("%d%d",&N,&M);
for(int i=0;i<=N;i++) scanf("%lf",&A[i].Re);
for(int j=0;j<=M;j++) scanf("%lf",&B[j].Re);
for(L=1;L<N+M+1;L<<=1);
for(int i=1;i<L;i++){
rnk[i]=(rnk[i>>1]>>1);
if(i&1) rnk[i]|=(L>>1);
}
return;
}
void FFT(Cplx *a,int d){
for(int i=0;i<L;i++)
if(i<rnk[i]) swap(a[i],a[rnk[i]]);
for(int n=2;n<=L;n<<=1){
Cplx w1(cos(2.0*pi/n),d*sin(2.0*pi/n));
for(int i=0;i<L;i+=n){
Cplx wk(1,0);
for(int j=i;j<i+(n>>1);j++,wk=wk*w1){
Cplx t1=a[j],t2=a[j+(n>>1)]*wk;
a[j]=t1+t2;
a[j+(n>>1)]=t1-t2;
}
}
}
return;
}
void mul1(){
FFT(A,1);FFT(B,1);
for(int i=0;i<L;i++)
C[i]=A[i]*B[i];
FFT(C,-1);
for(int i=0;i<=N+M;i++)
printf("%d ",(int)(C[i].Re/L+0.5));
return;
}
//(a+bi)^2=(a^2-b^2)+(2ab)i
void mul2(){
for(int i=0;i<L;i++)
A[i].Im=B[i].Re;
FFT(A,1);
for(int i=0;i<L;i++)
C[i]=A[i]*A[i];
FFT(C,-1);
for(int i=0;i<=N+M;i++)
printf("%d ",(int)(C[i].Im/L/2.0+0.5));
return;
}
int main(){
Input();
mul2();
return 0;
}