FFT感性瞎扯

为了自己以后再用FFT时不再一脸懵X,本蒟蒻决定感性理解一下FFT。

FFT可以干啥?

把两个多项式乘在一起。
具体地说,对于两个多项式f:f(x)=Σi=0nfixig:g(x)=Σi=0mgixi,得到一个多项式h:h(x)=Σx=0n+mΣi=0xf(i)g(xi)
显然,如果暴力的话,是O(n2)的。
但是,FFT可以做到O(nlog2n)
我们一点一点把它扯个不明不白。

Part1.复数

只需要记住一点结论:
复数相乘时,模长相乘,辐角相加。
比如说,对于两个极角表示法的复数u(ul,uθ)v(vl,vθ),有w=uv=(ulvl,uθ+vθ)

Part2.单位根

(以下,默认n2的整数幂)
我们把单位圆上的某个复数(cos(2kπ/n),sin(2kπ/n))称作单位根ωnk。换句话说,就是极角表示法下的复数(1,2kπ/n)。当然,因为是在单位圆上,我们可以只关注它的辐角(2kπ/n)
如图。
![8次单位根](https://cdn.luogu.com.cn/upload/image_hosting/otcevc5c.png)
结论1:ωnk=ωnk+n
类比正负角。可以感性理解一下,就等于绕了一整圈,度数>2π的角。有ωnk=2kπ/n=2kπ/n+2π=2kπ/n+2nπ/n=2(k+n)π/n=ωnk+n
结论2:ωnk=ωnk+n/2
等于绕了半圈的角。
结论3:当k为偶数时,ωnk=ωn/2k/2这是最重要的结论。
ωnk=2kπ/n=2(k/2)π/(n/2)=ωn/2k/2

Part3.DFT

显然,n+1个点可以唯一确定一个n次多项式。
而我们如果知道fg上各n+m+1个点,就可以在O(n)时间內把它们乘在一起。对于每个点(x,f(x))(x,g(x)),新点即为(x,f(x)g(x))
DFT解决的就是将多项式f由系数表达转为点值表达。
开始推式子:
f(x)
=a0+a1x+a2x2+...+anxn
=(a0+a2x2+...+anxn)+(a1x+a3x3+...+an1xn1)
=(a0+a2x2+...+anxn)+x(a1+a3x2+...+an1xn2)
f1(x)=a0+a2x+...+anxn/2f2(x)=a1+a3x+...+an1xn/2
f(x)=f1(x2)+xf2(x2)
x=ωnk(k<n/2)
f(ωnk)
=f1((ωnk)2)+ωnkf2((ωnk)2)
=f1(ωn2k)+ωnkf2(ωn2k)
=f1(ωn/2k)+ωnkf2(ωn/2k)
再令x=ωnk+n/2(k<n/2)
f(ωnk+n/2)
=f1((ωnk+n/2)2)+ωnk+n/2f2((ωnk+n/2)2)
=f1(ωn2k+n)+ωnk+n/2f2(ωn2k+n)
=f1(ωn2k)+ωnk+n/2f2(ωn2k)
=f1(ωn/2k)ωnkf2(ωn/2k)
注意到什么了吗?
**f(ωnk)f(ωnk+n/2),结果只有最后一项的符号不同!!!!!!**
然后就可以分治了。只要知道f1(ωn/2k)f2(ωn/2k)的值,就可以直接得出f(ωnk)f(ωnk+n/2)!!!
因此我们可以令f1(ωn/2k)成为新的f(x),进一步递推。

Part4.IDFT

只需要将DFT中的ωnk改为ωnk即可。别忘了最后记得除上数组长度!!!

Part5.代码:

复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4000000;
int n,m,lim=1,t,res[MAXN];
const double pi=acos(-1);
struct cp{
 double x,y;
 cp(double u=0.0,double v=0.0){
  x=u,y=v;
 }
 friend cp operator +(const cp &lv,const cp &rv){
  return cp(lv.x+rv.x,lv.y+rv.y);
 }
 friend cp operator -(const cp &lv,const cp &rv){
  return cp(lv.x-rv.x,lv.y-rv.y);
 }
 friend cp operator *(const cp &lv,const cp &rv){
  return cp(lv.x*rv.x-lv.y*rv.y,lv.x*rv.y+lv.y*rv.x);
 }
}f[MAXN],g[MAXN];
void FFT(cp *a,int sz,int inv){
 if(sz==1)return;
 int md=sz>>1;
 static cp b[MAXN];
 for(int i=0;i<md;i++)b[i]=a[i<<1],b[i+md]=a[(i<<1)+1];
 for(int i=0;i<sz;i++)a[i]=b[i];
 FFT(a,md,inv),FFT(a+md,md,inv);
 for(int i=0;i<md;i++){
  cp x=cp(cos(2*pi*i/sz),inv*sin(2*pi*i/sz));
  b[i]=a[i]+x*a[i+md],b[i+md]=a[i]-x*a[i+md];
 }
 for(int i=0;i<sz;i++)a[i]=b[i];
}
int read(){
 char c=getchar();
 int x=0;
 while(c>'9'||c<'0')c=getchar();
 while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
 return x;
}
int main(){
 n=read(),m=read();
 for(int i=0,t;i<=n;i++)f[i].x=read();
 for(int i=0,t;i<=m;i++)g[i].x=read();
 while(lim<=n+m)lim<<=1;
 FFT(f,lim,1),FFT(g,lim,1);
 for(int i=0;i<lim;i++)f[i]=f[i]*g[i];
 FFT(f,lim,-1);
 for(int i=0;i<lim;i++)res[i]=(int)(f[i].x/lim+0.5);
 for(int i=0;i<=n+m;i++)printf("%d ",res[i]);
 return 0;
}
复制代码

 


Part6.非递归

在FFT时,一个位置上的数的最终位置,是把其位置的二进制表达翻转后的新位置。
即有:
(7)10=(111)2(111)2=(7)10
(13)10=(1101)2(1011)2=(11)10
(22)10=(10110)2(01101)2=(13)10
显然,当长度不同时,一个数可能翻转到不同的位置。
而它的终点位置,可以如此递推:
for(int i=0;i<lim;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(lg-1));

 

其中,lim是长度,lg=log2(lim)

Part7.最终非递归代码:

复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4000000;
int n,m,lim=1,t,res[MAXN],lg,rev[MAXN];
const double pi=acos(-1);
struct cp{
 double x,y;
 cp(double u=0.0,double v=0.0){
  x=u,y=v;
 }
 friend cp operator +(const cp &lv,const cp &rv){
  return cp(lv.x+rv.x,lv.y+rv.y);
 }
 friend cp operator -(const cp &lv,const cp &rv){
  return cp(lv.x-rv.x,lv.y-rv.y);
 }
 friend cp operator *(const cp &lv,const cp &rv){
  return cp(lv.x*rv.x-lv.y*rv.y,lv.x*rv.y+lv.y*rv.x);
 }
}f[MAXN],g[MAXN];
int read(){
 char c=getchar();
 int x=0;
 while(c>'9'||c<'0')c=getchar();
 while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
 return x;
}
void FFT(cp *a,int tp){
 for(int i=0;i<lim;i++)if(i<rev[i])swap(a[i],a[rev[i]]);
 for(int md=1;md<lim;md<<=1){
  cp rt=cp(cos(pi/md),tp*sin(pi/md));
  for(int stp=md<<1,pos=0;pos<lim;pos+=stp){
   cp w=cp(1,0);
   for(int i=0;i<md;i++,w=w*rt){
    cp x=a[pos+i],y=w*a[pos+md+i];
    a[pos+i]=x+y;
    a[pos+md+i]=x-y;
   }
  }
 }
}
int main(){
 n=read(),m=read();
 while(lim<=(n+m))lim<<=1,lg++;
 for(int i=0;i<lim;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(lg-1));
 for(int i=0;i<=n;i++)f[i].x=read();
 for(int i=0;i<=m;i++)g[i].x=read();
 FFT(f,1),FFT(g,1);
 for(int i=0;i<lim;i++)f[i]=f[i]*g[i];
 FFT(f,-1);
 for(int i=0;i<lim;i++)res[i]=(int)(f[i].x/lim+0.5);
 for(int i=0;i<=n+m;i++)printf("%d ",res[i]);
 return 0;
} 
复制代码

 

完结撒傅里叶~~~

![](https://cdn.luogu.com.cn/upload/image_hosting/4l5cvdy8.png)
posted @   Troverld  阅读(164)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示