【数学】快速傅里叶变换(FFT)

快速傅里叶变换(FFT)

FFT 是之前学的,现在过了比较久的时间,终于打算在回顾的时候系统地整理一篇笔记,有写错的部分请指出来啊 qwq。

卷积

卷积、旋积或褶积(英语:Convolution)是通过两个函数 fg​​ 生成第三个函数的一种数学算子

定义

f,g​ 在 R1​ 上可积,那么 h(x)=f(τ)g(xτ)dτ 称为 fg​ 的卷积

对于整系数多项式域,n1 次多项式 A,B 的相乘可以得到 h(x)=A(x)B(x)=i=02n2j+k=iajbk=i=02n2j=0iajbijxi,对应的卷积为 Ci=AjBij

系数表示法

即用多项式各项系数来刻画这个多项式,例如 n1 次多项式就可以写成这样:A(x)=a0+a1x+...+an1xn1

点值表示法

我们知道,n​​​​ 个不同的点可以确定一个 n1​​​​ 次的多项式,所以我们可以使用 n​​​​ 个(不同)点来刻画一个 n1​​​​ 次多项式。

这样做会有什么方便呢?

例如 f(x)=(x0,f(x0)),...(xn,f(xn)),g(x)=(x0,g(x0)),...(xn,g(xn)),那么它们的卷积 h(x)=(x0,f(x0)g(x0)),...(xn,f(xn)g(xn))

这意味着在系数表示法中需要 O(n2) 次的乘法运算在点值表示法中只需要 O(n) 次。

系数表示法转点值表示法(DFT)

下面考虑如何将 n1 次多项式从系数表示法转为点值表示法。

因为用普通的方法选取 n​​​​​ 个点然后将系数表示法转为点值表示法的复杂度为 O(n2)​​​​​(因为需要选 n​​​​​ 个点,然后对于每个点 x 需要计算共 n 项的结果),我们考虑如何优化这一步。

注意到满足 wn=1​​ 的单位根 w​ 有 n​ 个,故从这里入手。

我们记方程 wn=1 的第 k 个单位根为 wnk​。

方便起见,设 n​ 为 2​ 的幂(就算不是也可以看作是高次项的系数为 0​)。

A(x) 按照次数的奇偶性分别分成两组 F(x),G(x),并表示为 A(x)=F(x2)+xG(x2)

例如 A(x)=a0+a1x+a2x2+a3x3​,那么 F(x)=a0+a2x,G(x)=a1+a3x​。

x=wnk​ 代入 A(x)​,由复数的性质,

A(wnk)=F(wn2k)+wnkG(wn2k)​ ,类似地 A(wnk+n2)=F(wn2k)wnkG(wn2k)​。​

推导:

A(wnk+n2)=F(wn2k+n)+wnk+n2G(wn2k+n)=F(wn2k)+wnk+n2G(wn2k)=F(wn2k)wnkG(wn2k)​​​

可以发现对于两个相应的单位根 wnk,wnn2+k​,可以用对应的 F,G​ 算出(可以递归地实现这个过程),而且计算的范围折半,所以一共需要计算 O(logN)​ 层,每一层执行 O(n)​ 次运算,所以复杂度为 O(NlogN)​。

点值表示法转系数表示法(IDFT)

下面考虑如何将 n1​ 次多项式从点值表示法转为系数表示法。

因为对于每个点值 yi=k=0n1wnki,其中 i[0,n1]​​​​,我们可以写出等式:

[11111wn1wn2wnn11wn2wn4wn2(n1)1wnn1wn2(n1)wn(n1)(n1)][a0a1a2an1]=[y0y1y2yn1]

现在我们已经有向量 y​​ 了(就是右式),因此,如果要得到向量 a​​,只需要两边乘上 w​​​ 矩阵的即可。

这里的 w​​ 矩阵正是著名的范德蒙矩阵,它的逆正好是每一项都取倒数,然后除以 n​​。

因此有 ai=1nk=0n1wnki,其中 i[0,n1]​。

有没有发现 ai,yi​ 的形式非常接近?据此,我们可以在实现的时候在同一个函数中写出逆变换和正变换,然后在得到的结果 res 中除以 n 就可以了。(参照下面的代码)

至此,FFT 的基本原理讲述完毕,下面是优化。

位逆序置换

按照上文的讲述,如果不看下面的代码,那么编写出来的是递归版本,但是这个版本的常数太大了,因此运行起来的效果不好,故使用位逆序置换来降低常数。

我们看看递归过程是什么样的,以 n=8 为例:

{x0,x1,x2,x3,x4,x5,x6,x7}{x0,x2,x4,x6},{x1,x3,x5,x7}{x0,x4},{x2,x6},{x1,x5},{x3,x7}{x0},{x4},{x2},{x6},{x1},{x5},{x3},{x7}

这里就有一个非常神奇的规律:在最后一行中,原下标所对应的二进制数翻转正好是在最后一行的序数。例如 x6 的下标是 6=110(2),那么它的序数正好是 011(2)=3

据此,可以处理出 rev 数组,它记录的正是最后一行所有元素对应的下标。

简单地说,递归形式是自上而下地做 FFT,而利用位逆序置换我们可以自下而上地做 FFT,它们在实际运行中有着常数上的区别。

模板题及代码

https://www.luogu.com.cn/problem/P3803

给定一个 n 次多项式 F(x),和一个 m 次多项式 G(x)

请求出 F(x)G(x) 的卷积。

#include<bits/stdc++.h>
using namespace std;

const int N=3e5+5;
const double pi=acos(-1);

int n, m;

// 复数类
struct Complex{
	double x, y;
	Complex operator + (const Complex &o)const { return {x+o.x, y+o.y}; }
	Complex operator - (const Complex &o)const { return {x-o.x, y-o.y}; }
	Complex operator * (const Complex &o)const { return {x*o.x-y*o.y, x*o.y+y*o.x}; }
};

Complex a[N], b[N];
int res[N];

int rev[N], bit, tot;

void fft(Complex a[], int inv){ // inv 指示正变换、逆变换。
	for(int i=0; i<tot; i++) if(i<rev[i]) swap(a[i], a[rev[i]]);
	
	for(int mid=1; mid<tot; mid<<=1){
		auto w1=Complex({cos(pi/mid), inv*sin(pi/mid)});
		for(int i=0; i<tot; i+=mid*2){
			auto wk=Complex({1, 0});
			for(int j=0; j<mid; j++, wk=wk*w1){
				auto x=a[i+j], y=wk*a[i+j+mid];
				a[i+j]=x+y, a[i+j+mid]=x-y;
			}
		}
	}
}

int main(){
	cin>>n>>m;
	for(int i=0; i<=n; i++) cin>>a[i].x;
	for(int i=0; i<=m; i++) cin>>b[i].x;
	
	while((1<<bit)<n+m+1) bit++; // 结果次数分布在 [0, n+m] 内,一共有 n+m+1 位。
	
	tot=1<<bit; // 得到上文所说的 n
	
	for(int i=0; i<tot; i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
	
	fft(a, 1), fft(b, 1); // 正变换 DFT
	
	for(int i=0; i<tot; i++) a[i]=a[i]*b[i];
	
	fft(a, -1); // 逆变换 IDFT
	
	for(int i=0; i<=n+m; i++) res[i]=(int)(a[i].x/tot+0.5), printf("%d ", res[i]);
	
	return 0;
}
posted @   HinanawiTenshi  阅读(767)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 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】
点击右上角即可分享
微信分享提示