快速傅立叶变换(FFT)

多项式#

系数表示法#

f(x)为一个n1次多项式,则 f(x)=i=0n1aixi

其中aif(x)的系数,用这种方法计算两个多项式相乘(逐位相乘)复杂度为O(n2)

点值表示法#

根据小学知识,一个n1次多项式可以唯一地被n个点确定

即,如果我们知道了对于一个多项式的n个点(x1,y1),(x2,y2)(xn,yn)

那么这个多项式唯一满足,对任意1in,满足yi=j=0n1ajxij

那么用点值实现多项式相乘是什么复杂度呢?

首先我们需要选n个点,每个点需要求出其在多项式中的值,复杂度为O(n2)

然后把两个点值表示的多项式相乘,由于c(xi)=a(xi)b(xi),复杂度为O(n)

最后插值法用点值求出系数,复杂度为O(n2)(我还不会插值)

考虑如果可以快速实现点值转系数和系数转点值,岂不是可以快速计算多项式乘法(说的简单,你倒是告诉我怎么快速转化啊)

前置芝士#

复数#

定义虚数单位i=1a,b为实数,则形如a+bi的数叫复数

其中a为复数的实部,b为复数的虚部

在复平面中,复数可以被表示为向量,所以和向量具有很多相似的性质,我们也可以用向量来理解复数,但是复数具有更多性质,比如作为一个数代入多项式

其中模长定义为a2+b2,幅度定义为x轴正半轴到向量转角的有向角

复数运算法则:

加减法与向量相同,重点是乘法:

几何定义为:模长相乘,幅角相加

代数定义为:

(a+bi)(c+di)

=ac+adi+bci+bdi2

=(acbd)+(ad+bc)i

单位根#

我们首先定义圆心为坐标原点,半径为1的圆叫做单位圆

我们将这个圆n等分,得到n个圆上的点,以这n个圆上点的横坐标作为实部,纵坐标作为虚部,就得到了n个复数

网上扒的图片,侵删QwQ

首先我们不自找麻烦,以(1,0)作为这n个点的起点,记作ωn0,逆时针方向第k个点记作ωnk

根据模长相乘幅角相加,我们可以看出ωnkωn0k次方,其中ωn1被称为n次单位根

根据幅角,我们可以计算出ωnk表示的复数为coskn2π+isinkn2π

单位根具有一些性质:

1.ωnk=coskn2π+isinkn2π=e2πkin

证明:这个第一步到第二步由定义得出,第二步到第三步由欧拉公式得出

2.ω2n2k=ωnk

证明:ω2n2k=e2π2ki2n=e2πkin=ωnk

3.ωnk+n2=ωnk

证明:ωnk+n2=ω2n2k+n=ω2n2kω2nn=ωnk(cosπ+isinπ)=ωnk

4.ωn0=ωnn=1

证明:不用证了吧……

正文之前#

这段话有可能有助于您理解本算法:

傅立叶这个大神仙根本就没见过计算机长什么样,所以他提出的傅立叶变换和逆变换只是一种将系数转点值和将点值转系数的方法,没有任何降低复杂度的功效,至于快速傅立叶变换是后人再研究傅立叶变换发现的一种加速方法,是对DFTIDFT的优化

离散傅立叶变换(DFT)#

假设f(x)=i=0n1aixi

DFT(a)=(f(1),f(ωn),f(ωn2),,f(ωnn1))

通俗点说,就是对于一个系数表示法的多项式f(x),将(1,ωn,ωn2,,ωnn1)带入求出该多项式的点值表示法

离散傅立叶逆变换(IDFT)#

f(x)nn次单位根处的点值表示转化为系数表示

这里就可以回答,为什么我们要让n次单位根作为x代入多项式

假设(y0,y1,y2,,yn1)是多项式A(x)=i=0n1aixi的离散傅立叶变换

我们另有一个多项式B(x)=i=0n1=yixi

将上述n次单位根的倒数(1,ωn1,ωn2,,ωn(n1))代入B(x)得到新的离散傅立叶变换(z0,z1,z2,,zn1)

则我们发现

zk=i=0n1yi(ωnk)i

=i=0n1(j=0n1aj(ωni)j)(ωnk)i

=j=0n1aj(i=0n1(ωnjk)i)

对于i=0n1(ωnjk)i我们单独考虑:

jk=0

答案为n

jk

等比数列求和得到(ωnjk)n1ωnjk1=(ωnn)jk1ωnjk1=1jk1ωnjk1=0

所以

j=0n1aj(i=0n1(ωnjk)i)=nak

ak=zkn

得出结论:对于以A(x)的离散傅立叶变换作为系数的多项式B(x),取单位根的倒数(1,ωn1,ωn2,,ωn(n1))作为x代入,再将结果除以n即为A(x)的系数

这个结论实现了将多项式点值转化为系数

快速傅立叶变换#

我们一顿分析最后发现复杂度……仍然是O(n2)

我们学这破玩意的意义不就是降低复杂度嘛,所以我们接下来讲怎么降复杂度

我们先设A(x)=i=0n1aixi

我们将A(x)的下标按奇偶分类,得到

A(x)=(a0+a2x2++an2xx2)+(a1x+a3x3++an1xn1)

设两个多项式

A1(x)=a0+a2x++an2xn21

A2(x)=a1+a3x++an1xn21

那么就可以发现A(x)=A1(x2)+xA2(x2)

x=ωnk(k<n2)代入

A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)

=A1(ωn2k)+ωnkA2(ωn2k)

x=ωnk+n2(k<n2)代入

A(ωnk+n2)=A1(ωn2k+n)+ωnk+n2A2(ωn2k+n)

=A1(ωn2kωnn)ωnkA2(ωn2kωnn)

=A1(ωn2k)ωnkA2(ωn2k)

我们一点也不惊喜地发现,只要求出A1(x)A2(x)(ωn20,ωn21,,ωn2n21)的点值表示,就可以O(n)地求出A(x)(1,ωn,ωn2,,ωnn1)

所以我们可以递归实现O(nlogn)求解多项式乘法了

注意:我们假设fg=h,那么对于fg都要直接求出大于n+m+1个的2k个点值(由于分治要求,点数一定是2的整次幂)

code#

Copy
#include<bits/stdc++.h> using namespace std; namespace red{ #define mid ((l+r)>>1) #define eps (1e-8) inline int read() { int x=0;char ch,f=1; for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar()); if(ch=='-') f=0,ch=getchar(); while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();} return f?x:-x; } const int N=5e6+10; const double pi=acos(-1.0); int n,m; struct complex { double x,y; complex(double tx=0,double ty=0){x=tx,y=ty;} inline complex operator + (const complex t) const { return complex(x+t.x,y+t.y); } inline complex operator - (const complex t) const { return complex(x-t.x,y-t.y); } inline complex operator * (const complex t) const { return complex(x*t.x-y*t.y,x*t.y+y*t.x); } }a[N],b[N]; inline void fft(int limit,complex *a,int inv) { if(limit==1) return; complex a1[limit>>1],a2[limit>>1]; for(int i=0;i<limit;i+=2) { a1[i>>1]=a[i],a2[i>>1]=a[i+1]; } fft(limit>>1,a1,inv); fft(limit>>1,a2,inv); complex Wn=complex(cos(2.0*pi/limit),inv*sin(2.0*pi/limit)),w=complex(1,0); for(int i=0;i<(limit>>1);++i,w=w*Wn) { a[i]=a1[i]+w*a2[i]; a[i+(limit>>1)]=a1[i]-w*a2[i]; } } inline void main() { n=read(),m=read(); for(int i=0;i<=n;++i) a[i].x=read(); for(int i=0;i<=m;++i) b[i].x=read(); int limit=1; while(limit<=n+m) limit<<=1; fft(limit,a,1); fft(limit,b,1); for(int i=0;i<=limit;++i) { a[i]=a[i]*b[i]; } fft(limit,a,-1); for(int i=0;i<=n+m;++i) printf("%d ",(int)(a[i].x/limit+0.5)); } } signed main() { red::main(); return 0; }

然而我们发现好像有点慢

迭代优化#

众所周知递归比较慢,我们有没有什么方法可以用迭代代替递归呢?

扒图时间到

通过一顿找规律,我们根本不能发现,每个数字在分治后的位置就是它所在位置的二进制翻转

这个规律也有一个好听的名字,叫蝴蝶定理

那么我们只要预处理出每个数字在最后一次递归中的位置,然后自底向上合并,岂不是可以摆脱递归

Copy
#include<bits/stdc++.h> using namespace std; namespace red{ #define eps (1e-8) inline int read() { int x=0;char ch,f=1; for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar()); if(ch=='-') f=0,ch=getchar(); while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();} return f?x:-x; } const int N=5e6+10; const double pi=acos(-1.0); int n,m; int limit=1,len; int pos[N]; struct complex { double x,y; complex(double tx=0,double ty=0){x=tx,y=ty;} inline complex operator + (const complex t) const { return complex(x+t.x,y+t.y); } inline complex operator - (const complex t) const { return complex(x-t.x,y-t.y); } inline complex operator * (const complex t) const { return complex(x*t.x-y*t.y,x*t.y+y*t.x); } }a[N],b[N],buf[N]; inline void fft(complex *a,int inv) { for(int i=0;i<limit;++i) if(i<pos[i]) swap(a[i],a[pos[i]]); for(int mid=1;mid<limit;mid<<=1) { complex Wn(cos(pi/mid),inv*sin(pi/mid)); for(int r=mid<<1,j=0;j<limit;j+=r) { complex w(1,0); for(int k=0;k<mid;++k,w=w*Wn) { buf[j+k]=a[j+k]+w*a[j+k+mid]; buf[j+k+mid]=a[j+k]-w*a[j+k+mid]; } } for(int i=0;i<limit;++i) a[i]=buf[i]; } } inline void main() { n=read(),m=read(); for(int i=0;i<=n;++i) a[i].x=read(); for(int i=0;i<=m;++i) b[i].x=read(); while(limit<=n+m) limit<<=1,++len; for(int i=0;i<limit;++i) pos[i]=(pos[i>>1]>>1)|((i&1)<<(len-1)); fft(a,1); fft(b,1); for(int i=0;i<=limit;++i) a[i]=a[i]*b[i]; fft(a,-1); for(int i=0;i<=n+m;++i) printf("%d ",(int)(a[i].x/limit+0.5)); } } signed main() { red::main(); return 0; }

蝴蝶操作#

考虑这里

Copy
for(int r=mid<<1,j=0;j<limit;j+=r) { complex w(1,0); for(int k=0;k<mid;++k,w=w*Wn) { buf[j+k]=a[j+k]+w*a[j+k+mid]; buf[j+k+mid]=a[j+k]-w*a[j+k+mid]; } } for(int i=0;i<limit;++i) a[i]=buf[i];

之所以加buf数组是因为两次赋值a的值会变化,我们可以提前存储a数组的值,然后优化掉buf数组

Copy
for(int k=0;k<mid;++k,w=w*Wn) { complex x=a[j+k],y=w*a[j+k+mid]; a[j+k]=x+y; a[j+k+mid]=x-y; }

三次变两次优化#

观察到上面的代码我们跑了三次肥肥兔,现在我们有一种方法可以少跑一次

假设我们求f(x)g(x)

设复多项式h(x)=f(x)+ig(x),实部为f(x),虚部为g(x)

那么h(x)2=(f(x)+ig(x))2=f(x)2g(x)2+i2f(x)g(x)

我们只要把h(x)2的虚部除以2就得到了结果

完全版:

Copy
#include<bits/stdc++.h> using namespace std; namespace red{ #define eps (1e-8) inline int read() { int x=0;char ch,f=1; for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar()); if(ch=='-') f=0,ch=getchar(); while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();} return f?x:-x; } const int N=5e6+10; const double pi=acos(-1.0); int n,m; int limit=1,len; int pos[N]; struct complex { double x,y; complex(double tx=0,double ty=0){x=tx,y=ty;} inline complex operator + (const complex t) const { return complex(x+t.x,y+t.y); } inline complex operator - (const complex t) const { return complex(x-t.x,y-t.y); } inline complex operator * (const complex t) const { return complex(x*t.x-y*t.y,x*t.y+y*t.x); } }a[N]; inline void fft(complex *a,int inv) { for(int i=0;i<limit;++i) if(i<pos[i]) swap(a[i],a[pos[i]]); for(int mid=1;mid<limit;mid<<=1) { complex Wn(cos(pi/mid),inv*sin(pi/mid)); for(int r=mid<<1,j=0;j<limit;j+=r) { complex w(1,0); for(int k=0;k<mid;++k,w=w*Wn) { complex x=a[j+k],y=w*a[j+k+mid]; a[j+k]=x+y; a[j+k+mid]=x-y; } } } } inline void main() { n=read(),m=read(); for(int i=0;i<=n;++i) a[i].x=read(); for(int i=0;i<=m;++i) a[i].y=read(); while(limit<=n+m) limit<<=1,++len; for(int i=0;i<limit;++i) pos[i]=(pos[i>>1]>>1)|((i&1)<<(len-1)); fft(a,1); for(int i=0;i<=limit;++i) a[i]=a[i]*a[i]; fft(a,-1); for(int i=0;i<=n+m;++i) printf("%d ",(int)(a[i].y/limit/2+0.5)); } } signed main() { red::main(); return 0; }

注意三次变两次优化会令精度误差平方,请根据题目值域考虑是否使用

参考博客
%%%attack
%%%rabbithu

一些例题#

不定期更新
洛谷P1919 A*B Problem升级版
洛谷P3338 [ZJOI2014]力

posted @   lovelyred  阅读(1150)  评论(12编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示
CONTENTS