快速傅里叶变换(FFT)——从入门到入土
FFT (快速傅里叶变换)——从入门到入土
请不要转载,谢谢。版权于江苏省前黄高级中学 刘成宇 手中
前置知识
多项式系数/点值表达法
对于一个多项式
明显是一个 次函数/多项式,给这种对于 对应关系 的种类一个名称——多项式的系数表达法。
还有什么表示方式呢?下面给出一条引理
用 个点可以确定一条 次函数图像。
感性证明:
这时候注意到,如果我现在 那么我是不是用加减消元法最终可以确定
那么这时候就简单了,可以用点刻画函数。即为点值表示法。用形如 表示
复数计算
如果两个复数相乘
如果两个复数相加或相减比较简单,这里就不给出了。
复平面
本文令
快速傅里叶变换是基于复数的,复平面是其中一个基本的知识。
注意,原点是不在虚轴上的,因为 而 是实数
就类似于上面这样一个平面。对于平面上任意一个点 其都表示一个复数 。
如果我现在要对复平面上的一个点 平方。
因为在单位圆上 所以有 即
首先计算它的值
新的点 计算一下模长
所以,现在的这个点仍然在单位圆上。
接下来考虑一下它与原点连线后的旋转角 与原旋转角 的关系。
由于原来的点坐标是 所以有 不妨令
那么综合现在的坐标就有 类似于 whk 中处理的套路,齐次式将上边的除下去。
不难看出二倍角公式,那么有
就相当于多转了一个 这种特殊性就是 FFT 为什么所撷取的。
真正的FFT
FFT 正变换
快速傅里叶变换,处理多项式乘法时非常有用。既然是多项式乘法,那不可能是用一般的系数表达式一个一个乘,那么得想一个更好的办法,由于:
对于点值表达法计算 答案
明显发现对于点值表达法计算乘法只需要 如此优秀的时间就 ok 了,那么直接使用点值表达法就 ok 啦。这时候遇到的最大的一个问题就是,对于直观的系数表达法与抽象但好用的点值表达法之间,我如何去转化,这一点是值得我们去思考的。这也是 FFT 区别于或者说优于 DFT 的一个特点。
现在就是要计算 个 且这些 互不相同,总时间复杂度还不能有 怎么办。
先把 转化成一个大于 二的整数次幂的形式,不够的高位上系数补 ,然后进行操作。
复数就派上用场了,选取的就是 这个时候因为这里的 肯定是大于原来的 的,所以即使取 个点也是可以计算出原来的值的。
现在就是要计算 了。
首先,稍微的将 展开看看 这时候做这样一步构造
令
显然有
将 写出来看看
好的,但是问题并没有解决,即使化成这种形式,还是找不到任何优化的点。这时候分类讨论一波
- 如果当 时,我就用算就 ok 了。
- 如果当 时,不妨令 那么这个 又回到了上一个情况了,就有
首先
并且
那么就有
连起来
发现了特殊的地方,两个的差距只有一个 ,那么就其实我们可以通过计算 以及 就能求出 以及
那么求 就被切成了一半,只需要求一半就 ok 了。
这个时候观察一下所要求的 也是一个可以使用这种方法的式子,那么使用这种同样的方法就可以了。
有点像线段树分治一样。
FFT 负变换
上面的正变换只是一半,是把这个东西转化成点值表达法的方式,由于 正常 的题目最后肯定还是要你转化成系数表达式的,那么还需还需要进行将点值表达式转化为系数表达式。
怎么操作呢,确实很难,但是总有大佬能想到。是一种构造法。
令 其中 就是上面的点值表达式中的所有纵坐标
先给出结论把
证明一波:
由于
代入
像莫比乌斯反演一样套路调换顺序
整理整理
这时候思考一下后面的东西
令
两式相减
将 代入
显然,上式为 ,但是,除非 即分母为 时,思考一下什么意思就是 时呗。根据定义,此时
好的,处理了这些,将 代回去
发现当且仅当 时原式才有值。
证到这里,两边再除以 ,答案就出来了。
当然,为了求值,再回眸看一眼这个 有无何特殊的地方,这不就是刚才算的正变换把 换成了 么?
所以,正变换和逆变换是两个相反的运算,从某种方面理解,就是多转了半圈。
FFT 递归版 die 码
#include <bits/stdc++.h>
#define debug puts("I ak IOI several times");
#define pb push_back
using namespace std;
template <typename T>inline void read(T& t){
t=0; register char ch=getchar(); register int fflag=1;
while(!('0'<=ch&&ch<='9')){if(ch=='-') fflag=-1;ch=getchar();}
while(('0'<=ch&&ch<='9')){t=((t<<1)+(t<<3))+ch-'0'; ch=getchar();} t*=fflag;
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){
read(t);read(args...);
}
template <typename T>inline void write(T x){
if(x<0) putchar('-'),x=~(x-1); int s[40],top=0;
while(x) s[++top]=x%10,x/=10; if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
}
const double PI=acos(-1.0);
const int MAXN=2500005;
struct Complex{
double x,y;
Complex(double _x=0,double _y=0){x=_x,y=_y;}
}a[MAXN],b[MAXN],c[MAXN];
Complex operator + (Complex a,Complex b){return Complex(a.x+b.x,a.y+b.y);}
Complex operator - (Complex a,Complex b){return Complex(a.x-b.x,a.y-b.y);}
Complex operator * (Complex a,Complex b){return Complex(a.x*b.x-a.y*b.y,a.y*b.x+a.x*b.y);}
void FFT(Complex *y,int len,int type){
if(len<=1) return;
int mid=len>>1;
Complex a1[mid],a2[mid];
for(int i=0;i<mid;++i) a1[i]=y[i<<1],a2[i]=y[i<<1|1];
FFT(a1,mid,type);FFT(a2,mid,type);
Complex Wn=Complex(cos(2*PI/len),type*sin(2*PI/len)),w(1,0);
for(int i=0;i<mid;++i){
Complex wt=w*a2[i];
y[i]=a1[i]+wt;
y[i+mid]=a1[i]-wt;
w=w*Wn;
}
}
int n,m,limit;
int main(){
read(n,m);
for(int i=0;i<=n;++i) cin>>a[i].x;
for(int i=0;i<=m;++i) cin>>b[i].x;
limit=1;while(limit<=n+m) limit<<=1;
FFT(a,limit,1); FFT(b,limit,1);
for(int i=0;i<=limit;++i) c[i]=a[i]*b[i];
FFT(c,limit,-1);
for(int i=0;i<=n+m;++i) cout<<(int)(c[i].x/limit+0.5)<<' ';
return 0;
}
//Welcome back,Chtholly.
递推优化 FFT
蝴蝶操作
观察到在原来的操作中有这样一步。
Complex wt=w*a2[i];
y[i]=a1[i]+wt;
y[i+mid]=a1[i]-wt;
w=w*Wn;
现在称其为蝴蝶操作
类似于下图
发现规律
对于原来的递归版 FFT ,不难发现,很多一大部分是将数组拆分的过程,那这个数组拆分的有没有规律呢?
还真有,这里继续引用一下谷里的题解图片。
这时候把最后一行全部转成二进制:
reverse一下
转成十进制发现就是 那么这个规律好用了呀。
只需要提前构造一下数组就 ok 了,将数组变成 的模样似乎就不用拆分了? 是的。
这里想一下如何 reserve 。
令 号反转成 。
如果我正着做,显然 时已经处理好了的。
想想 和 有没有什么关系。
不就是 右移一位嘛,那么它如果全部反转的话应该在第一位。好了,那么只需要看原来的 是不是 就行了,是 把开头一位换成 .
for(int i=0;i<limit;++i) rev[i]=(rev[i>>1]>>1)|((i&1)*(1<<L-1));
die 码一行,简洁易懂常数小
迭代 FFT
知道了这些,迭代 FFT 只不过是一步之遥。只需要想想 die 码的细节就好了,这里给出一张宝贵的图。
我感觉这张图已经非常好理解力。
下面上 die 码:
#include <bits/stdc++.h>
#define debug puts("I ak IOI several times");
#define pb push_back
using namespace std;
template <typename T>inline void read(T& t){
t=0; register char ch=getchar(); register int fflag=1;
while(!('0'<=ch&&ch<='9')){if(ch=='-') fflag=-1;ch=getchar();}
while(('0'<=ch&&ch<='9')){t=((t<<1)+(t<<3))+ch-'0'; ch=getchar();} t*=fflag;
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){
read(t);read(args...);
}
template <typename T>inline void write(T x){
if(x<0) putchar('-'),x=~(x-1); int s[40],top=0;
while(x) s[++top]=x%10,x/=10; if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
}
const double PI=acos(-1.0);
const int MAXN=2500005;
struct Complex{
double x,y;
Complex(double _x=0,double _y=0){x=_x,y=_y;}
}a[MAXN],b[MAXN],c[MAXN];
Complex operator + (Complex a,Complex b){return Complex(a.x+b.x,a.y+b.y);}
Complex operator - (Complex a,Complex b){return Complex(a.x-b.x,a.y-b.y);}
Complex operator * (Complex a,Complex b){return Complex(a.x*b.x-a.y*b.y,a.y*b.x+a.x*b.y);}
int rev[MAXN],limit,L,n,m;
void FFT(Complex *y,int type){
for(int i=0;i<limit;++i) if(rev[i]>i) swap(y[i],y[rev[i]]);
for(int len=2;len<=limit;len<<=1){
Complex Wn(cos(2*PI/len),type*sin(2*PI/len));
int mid=len>>1;
for(int L=0,R=len-1;R<=limit;L+=len,R+=len){
Complex w(1,0);
for(int p=L;p<L+mid;++p){
Complex X=y[p]+w*y[p+mid],Y=y[p]-w*y[p+mid];
y[p]=X;y[p+mid]=Y;
w=w*Wn;
}
}
}
}
int main(){
read(n,m);
for(int i=0;i<=n;++i) cin>>a[i].x;
for(int i=0;i<=m;++i) cin>>b[i].x;
limit=1;while(limit<=n+m) limit<<=1,++L;
for(int i=0;i<limit;++i) rev[i]=(rev[i>>1]>>1)|((i&1)*(1<<L-1));
FFT(a,1);FFT(b,1);
for(int i=0;i<=limit;++i) c[i]=a[i]*b[i];
FFT(c,-1);
for(int i=0;i<=n+m;++i) cout<<(int)(c[i].x/limit+0.5)<<' ';
return 0;
}
//Welcome back,Chtholly.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通