【瞎口胡】快速傅里叶变换 / FFT
快速傅里叶变换(FFT)是一种在 时间复杂度内求出两个 次多项式乘积的算法。
系数表示法和点值表示法#
对于 次多项式 ,如果我们知道了每一个 ,那么这个多项式就唯一确定。于是我们用系数序列 来表示这个多项式,这被称作系数表示法。
而我们也可以取这个多项式在 个不同的 处的取值来表示这个多项式。根据高斯消元法,这 个 以及 的取值可以确定这个多项式。这被称作点值表示法。
一个多项式从系数表示法转换为点值表示法的过程,被称作离散傅里叶变换(DFT)。反之则是离散傅里叶逆变换(IDFT)。
FFT 取一些特殊的 ,来加速 DFT 和 IDFT 的过程。这些 甚至不在实数域,而是一些复数。
特殊的复数#
在复平面上,一个以原点为圆心,半径为 的圆被称作单位圆。从实轴( 轴)正方向开始,逆时针作 个向量将单位圆 等分,则这些向量与单位圆相交形成的 个交点被称作 次复根,第一个辐角为正的向量与单位圆的交点被称作 次单位复根,记作 。
根据复数的乘法运算法则「模长相乘,辐角相加」,可知 次复根的 次方都是 。而所有 次复根都可以用 次单位复根的幂表示。
我们知道,。根据三角函数的知识, 的实部即为 ,虚部为 。于是,。
单位复根有一些性质:
- 对于 ,
这些奇妙的性质将会在接下来的环节中充分发挥作用。
快速傅里叶变换 / FFT#
在 FFT 中,我们将 依次 带入求值,便得到了一个 次多项式的点值表示法。
但这样不够快,我们考虑分治。
对于 ,设 次多项式
设
则有
带入 :
带入 :
因此当我们求出了 之后,就可以求出 。该算法的复杂度为 。
但是这样要递归,不够快!于是我们考虑优化。我们如果能求出每个系数最后到了哪个位置,就可以不断地合并这些系数,然后求解。
对于 ,我们来看看每次递归之后,系数是怎么被分类的:
把下标用二进制表示
我们观察到,原来在第 (从 开始)个位置的系数,最后变到了二进制翻转之后的那个位置。举个例子, 在现在在第 个位置。,翻转之后就是 。
记 为 二进制翻转之后的值。我们递推求 。已知 。当 时, 已知。我们考虑它在二进制下与 的关系:
其中 为 位 串, 为 位 串, 表示连接两个 串。
为什么是 呢?观察到 的最高位一定是 ,所以翻转之后的最低位一定是 。
同时,观察到 是翻转后的最高位,即 的最低位。
于是,我们得到了 的递推式:
其中 满足 FFT 的长度 。
这个操作叫做蝴蝶变换,也称位逆序变换。
inline void change(Poly &f,int len){
for(int i=0;i<=len;++i){
rev[i]=(rev[i>>1]>>1);
if(i&1){
rev[i]|=(len>>1);
}
}
for(int i=0;i<=len;++i){ // 保证每一个对只会被翻转一次
if(i<rev[i]){
std::swap(f[i],f[rev[i]]); // 直接将系数扔到最后的位置 然后 FFT
}
}
return;
}
点值相乘#
在求出多项式 的 组点值后,我们要求 的 组点值。显然,。于是我们对求出点值进行对位相乘的操作,就得到 的 组点值。
下文中,我们会讲解如何通过带入的 组特殊点值还原出多项式本身。容易发现,对 进行这个过程,还原出的多项式就是 。
快速傅里叶逆变换 / IFFT#
我们通过 FFT 求出了 组点值。记它们为 ,其中 。
设多项式
则取 对 进行 FFT,得到的点值序列就是原来 序列的 倍。
接下来来证明一下:
后面的和式在 时值为 ,此时 ,由这两个和式的范围可得 。
在 (此时 )时, 是一个等比数列求和
由单位复根的性质得该式值为 。
则我们继续推导,
于是我们只需要在 FFT 时将单位根变为 ,再进行 FFT,就完成了 IFFT。
# include <bits/stdc++.h>
const int N=4000010;
struct Complex{
double x,y;
Complex(double _x=0.0,double _y=0.0){
x=_x,y=_y;
return;
}
Complex operator + (const Complex &b) const{
return Complex(x+b.x,y+b.y);
}
Complex operator - (const Complex &b) const{
return Complex(x-b.x,y-b.y);
}
Complex operator * (const Complex &b) const{
return Complex(x*b.x-y*b.y,x*b.y+y*b.x);
}
};
typedef std::vector <Complex> Poly;
const double PI=acos(-1.0);
int rev[N];
Poly F,G;
int n,m;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline void change(Poly &f,int len){ // 蝴蝶变换
for(int i=0;i<=len;++i){
rev[i]=(rev[i>>1]>>1);
if(i&1){
rev[i]|=(len>>1);
}
}
for(int i=0;i<=len;++i){
if(i<rev[i]){
std::swap(f[i],f[rev[i]]);
}
}
return;
}
inline void fft(Poly &f,int len,double op){
change(f,len);
for(int h=2;h<=len;h<<=1){
Complex wn(cos(2*PI/h),sin(op*2*PI/h));
for(int j=0;j<len;j+=h){
Complex w(1,0);
for(int k=j;k<j+h/2;++k){
Complex u=f[k],t=w*f[k+h/2];
f[k]=u+t,f[k+h/2]=u-t;
w=w*wn;
}
}
}
return;
}
int main(void){
n=read(),m=read();
int maxlen=1;
while(maxlen<=n+m){ // 注意是 <= 而非 <
maxlen<<=1;
}
F.resize(maxlen+5),G.resize(maxlen+5);
for(int i=0;i<=n;++i){
F[i]=Complex(read(),0);
}
for(int i=0;i<=m;++i){
G[i]=Complex(read(),0);
}
fft(F,maxlen,1),fft(G,maxlen,1);
for(int i=0;i<=maxlen;++i){
F[i]=F[i]*G[i];
}
fft(F,maxlen,-1);
for(int i=0;i<=n+m;++i){
printf("%d ",(int)(F[i].x/maxlen+0.5));
}
return 0;
}
作者:Meatherm
出处:https://www.cnblogs.com/Meatherm/p/14960829.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探