快速傅里叶变换(FFT)——从入门到入土

FFT (快速傅里叶变换)——从入门到入土

请不要转载,谢谢。版权于江苏省前黄高级中学 刘成宇 手中

前置知识

多项式系数/点值表达法

对于一个多项式 A(x)=i=0n1aixi

明显是一个 n1 次函数/多项式,给这种对于 对应关系 A 的种类一个名称——多项式的系数表达法。

还有什么表示方式呢?下面给出一条引理

n+1 个点可以确定一条 n 次函数图像。

感性证明:

{y1=a0+a1x1+...+an1x1n1y2=a0+a1x2+...+an1x2n1y3=a0+a1x3+...+an1x3n1...

这时候注意到,如果我现在 x1x2x3...xn+1 那么我是不是用加减消元法最终可以确定 a0,a1,a2,...,an1

那么这时候就简单了,可以用点刻画函数。即为点值表示法。用形如 A(x)={(x1,y1),(x2,y2),...,(xn,yn)} 表示

复数计算

如果两个复数相乘 (a+bi)(c+di)=acbd+(bc+ad)i

如果两个复数相加或相减比较简单,这里就不给出了。

复平面

本文令 i=1

快速傅里叶变换是基于复数的,复平面是其中一个基本的知识。

image

注意,原点是不在虚轴上的,因为 0i=00 是实数

就类似于上面这样一个平面。对于平面上任意一个点 (a,b) 其都表示一个复数 a+bi

如果我现在要对复平面上的一个点 (a,b) 平方。

因为在单位圆上 所以有 |(a,b)|=a2+b2=1a2+b2=1

首先计算它的值 (a+bi)2=a2b2+2abi

新的点 (a2b2,2ab) 计算一下模长

|(a2b2,2ab)|=(a2b2)2+4a2b2=a4+b42a2b2+4a2b2=a4+b4+2a2b2=(a2+b2)2=1

所以,现在的这个点仍然在单位圆上。

接下来考虑一下它与原点连线后的旋转角 θ2 与原旋转角 θ1 的关系。

由于原来的点坐标是 (a,b) 所以有 tanθ1=ba 不妨令 ba=λ

那么综合现在的坐标就有 tanθ2=2aba2b2 类似于 whk 中处理的套路,齐次式将上边的除下去。

tanθ2=2abba=2λ1λ2 不难看出二倍角公式,那么有

θ2=2θ1 就相当于多转了一个 θ1 这种特殊性就是 FFT 为什么所撷取的。

真正的FFT

FFT 正变换

快速傅里叶变换,处理多项式乘法时非常有用。既然是多项式乘法,那不可能是用一般的系数表达式一个一个乘,那么得想一个更好的办法,由于:

对于点值表达法计算 A(i)×B(i) 答案 C(i)={(x0,Ax0Bx0),(x1,Ax1Bx1),(x2,Ax2Bx2),...,(xn,AnBn)}

明显发现对于点值表达法计算乘法只需要 O(n) 如此优秀的时间就 ok 了,那么直接使用点值表达法就 ok 啦。这时候遇到的最大的一个问题就是,对于直观的系数表达法与抽象但好用的点值表达法之间,我如何去转化,这一点是值得我们去思考的。这也是 FFT 区别于或者说优于 DFT 的一个特点。

现在就是要计算 nA(x) 且这些 x 互不相同,总时间复杂度还不能有 O(n2) 怎么办。

先把 n 转化成一个大于 n 二的整数次幂的形式,不够的高位上系数补 0 ,然后进行操作。

复数就派上用场了,选取的就是 ωnkk[0,n) 这个时候因为这里的 n 肯定是大于原来的 n 的,所以即使取 n 个点也是可以计算出原来的值的。

现在就是要计算 A(ωnk) 了。

首先,稍微的将 A(x)=i=0n1aixi 展开看看 A(x)=a0+a1x+a2x2+...+an1xn1 这时候做这样一步构造

A1(x)=a0+a2x+a4x2+...+an2xn21
A2(x)=a1+a3x+a5x2+...+an1xn21

显然有 A(x)=A1(x2)+xA2(x2)
A(ωnk) 写出来看看

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

好的,但是问题并没有解决,即使化成这种形式,还是找不到任何优化的点。这时候分类讨论一波

  1. 如果当 0k<n2 时,我就用A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)算就 ok 了。
  2. 如果当 n>kn2 时,不妨令 t=k+n2 那么这个 t 又回到了上一个情况了,就有 A(k)=A1(ωn2k+n)+ωnk+n2A2(ωn2k+n)

首先 ωn2k+n=ωn2kωnn=ωn2k
并且 ωnk+n2=ωnk
那么就有 A(k)=A1(ωn2k)ωnkA2(ωn2k)=A1(ωn2k)ωnkA2(ωn2k)

连起来

A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)k[0,n2)
A(ωnk+n2)=A1(ωn2k)ωnkA2(ωn2k)k[n2,n)

发现了特殊的地方,两个的差距只有一个 ωnkA2(ωn2k) ,那么就其实我们可以通过计算 A1(ωn2k) 以及 A2(ωn2k)ωnk 就能求出 A(ωnk) 以及 A(ωnk+n2)

那么求 A(1),A(2),...,A(n) 就被切成了一半,只需要求一半就 ok 了。

这个时候观察一下所要求的 A1(x),A2(x) 也是一个可以使用这种方法的式子,那么使用这种同样的方法就可以了。

有点像线段树分治一样。

FFT 负变换

上面的正变换只是一半,是把这个东西转化成点值表达法的方式,由于 正常 的题目最后肯定还是要你转化成系数表达式的,那么还需还需要进行将点值表达式转化为系数表达式。

怎么操作呢,确实很难,但是总有大佬能想到。是一种构造法。

f(x)=i=0nyixi 其中 yi 就是上面的点值表达式中的所有纵坐标

先给出结论把

ak=f(ωnk )n

证明一波:

f(ωnk)=i=0nyiωnik

由于 yi=j=0nωnijaj

代入

f(ωnk)=i=0n[j=0nωnijaj]ωnik

像莫比乌斯反演一样套路调换顺序

f(ωnk)=j=0ni=0n[ωnijaj]ωnik

整理整理

f(ωnk)=j=0naji=0n(ωnjk)i

这时候思考一下后面的东西

S(x)=1+x+x2+...+xn1
xS(x)=x+x2+x3+...+xn
两式相减 (x1)S(x)=xn1
S(x)=xn 1x1

x=ωnu 代入

S(ωnu)=(ωnn)u1ωnu1=11ωnu1

显然,上式为 0 ,但是,除非 ωnu1=0 即分母为 0 时,思考一下什么意思就是 n=u 时呗。根据定义,此时 S(ωnu)=n

好的,处理了这些,将 S(x) 代回去

f(ωnk)=j=0naji=0n(ωnjk)i

发现当且仅当 j=k 时原式才有值。 f(ωnk)=akn

证到这里,两边再除以 n ,答案就出来了。

当然,为了求值,再回眸看一眼这个 f(ωnk) 有无何特殊的地方,这不就是刚才算的正变换把 ωnk 换成了 ωnk 么?

所以,正变换和逆变换是两个相反的运算,从某种方面理解,就是多转了半圈。

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;

现在称其为蝴蝶操作
类似于下图

image

发现规律

对于原来的递归版 FFT ,不难发现,很多一大部分是将数组拆分的过程,那这个数组拆分的有没有规律呢?

还真有,这里继续引用一下谷里的题解图片。

image

这时候把最后一行全部转成二进制:

000,100,010,110,001,101,011,111

reverse一下

000,001,010,011,100,101,110,111

转成十进制发现就是 0,1,2,3,4,5,6,7 那么这个规律好用了呀。

只需要提前构造一下数组就 ok 了,将数组变成 0,4,2,6,1,5,3,7 的模样似乎就不用拆分了? 是的。

这里想一下如何 reserve 。

i 号反转成 revi

如果我正着做,显然 revi2 时已经处理好了的。

想想 revi2revi 有没有什么关系。

i2 不就是 i 右移一位嘛,那么它如果全部反转的话应该在第一位。好了,那么只需要看原来的 i&1 是不是 1 就行了,是 1 把开头一位换成 1 .

for(int i=0;i<limit;++i) rev[i]=(rev[i>>1]>>1)|((i&1)*(1<<L-1));

die 码一行,简洁易懂常数小

迭代 FFT

知道了这些,迭代 FFT 只不过是一步之遥。只需要想想 die 码的细节就好了,这里给出一张宝贵的图。

image

我感觉这张图已经非常好理解力。

下面上 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.
posted @   Mercury_City  阅读(388)  评论(0编辑  收藏  举报
编辑推荐:
· 基于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最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示