快速傅立叶变换(FFT)
多项式#
系数表示法#
设为一个次多项式,则
其中为的系数,用这种方法计算两个多项式相乘(逐位相乘)复杂度为
点值表示法#
根据小学知识,一个次多项式可以唯一地被个点确定
即,如果我们知道了对于一个多项式的个点
那么这个多项式唯一满足,对任意,满足
那么用点值实现多项式相乘是什么复杂度呢?
首先我们需要选个点,每个点需要求出其在多项式中的值,复杂度为
然后把两个点值表示的多项式相乘,由于,复杂度为
最后插值法用点值求出系数,复杂度为(我还不会插值)
考虑如果可以快速实现点值转系数和系数转点值,岂不是可以快速计算多项式乘法(说的简单,你倒是告诉我怎么快速转化啊)
前置芝士#
复数#
定义虚数单位,为实数,则形如的数叫复数
其中为复数的实部,为复数的虚部
在复平面中,复数可以被表示为向量,所以和向量具有很多相似的性质,我们也可以用向量来理解复数,但是复数具有更多性质,比如作为一个数代入多项式
其中模长定义为,幅度定义为轴正半轴到向量转角的有向角
复数运算法则:
加减法与向量相同,重点是乘法:
几何定义为:模长相乘,幅角相加
代数定义为:
单位根#
我们首先定义圆心为坐标原点,半径为的圆叫做单位圆
我们将这个圆等分,得到个圆上的点,以这个圆上点的横坐标作为实部,纵坐标作为虚部,就得到了个复数
网上扒的图片,侵删
首先我们不自找麻烦,以作为这个点的起点,记作,逆时针方向第个点记作
根据模长相乘幅角相加,我们可以看出是的次方,其中被称为次单位根
根据幅角,我们可以计算出表示的复数为
单位根具有一些性质:
证明:这个第一步到第二步由定义得出,第二步到第三步由欧拉公式得出
证明:
证明:
证明:不用证了吧……
正文之前#
这段话有可能有助于您理解本算法:
傅立叶这个大神仙根本就没见过计算机长什么样,所以他提出的傅立叶变换和逆变换只是一种将系数转点值和将点值转系数的方法,没有任何降低复杂度的功效,至于快速傅立叶变换是后人再研究傅立叶变换发现的一种加速方法,是对和的优化
离散傅立叶变换(DFT)#
假设
通俗点说,就是对于一个系数表示法的多项式,将带入求出该多项式的点值表示法
离散傅立叶逆变换(IDFT)#
将在个次单位根处的点值表示转化为系数表示
这里就可以回答,为什么我们要让次单位根作为代入多项式
假设是多项式的离散傅立叶变换
我们另有一个多项式
将上述次单位根的倒数代入得到新的离散傅立叶变换
则我们发现
对于我们单独考虑:
当时
答案为
当时
等比数列求和得到
所以
即
得出结论:对于以的离散傅立叶变换作为系数的多项式,取单位根的倒数作为代入,再将结果除以即为的系数
这个结论实现了将多项式点值转化为系数
快速傅立叶变换#
我们一顿分析最后发现复杂度……仍然是
我们学这破玩意的意义不就是降低复杂度嘛,所以我们接下来讲怎么降复杂度
我们先设
我们将的下标按奇偶分类,得到
设两个多项式
那么就可以发现
将代入
将代入
我们一点也不惊喜地发现,只要求出和在的点值表示,就可以地求出在
所以我们可以递归实现求解多项式乘法了
注意:我们假设,那么对于和都要直接求出大于个的个点值(由于分治要求,点数一定是的整次幂)
#
#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;
}
然而我们发现好像有点慢
迭代优化#
众所周知递归比较慢,我们有没有什么方法可以用迭代代替递归呢?
通过一顿找规律,我们根本不能发现,每个数字在分治后的位置就是它所在位置的二进制翻转
这个规律也有一个好听的名字,叫蝴蝶定理
那么我们只要预处理出每个数字在最后一次递归中的位置,然后自底向上合并,岂不是可以摆脱递归
#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;
}
蝴蝶操作#
考虑这里
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];
之所以加数组是因为两次赋值的值会变化,我们可以提前存储数组的值,然后优化掉数组
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;
}
三次变两次优化#
观察到上面的代码我们跑了三次肥肥兔,现在我们有一种方法可以少跑一次
假设我们求
设复多项式,实部为,虚部为
那么
我们只要把的虚部除以就得到了结果
完全版:
#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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 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】