【瞎口胡】快速傅里叶变换 / FFT

快速傅里叶变换(FFT)是一种在 O(nlogn) 时间复杂度内求出两个 n 次多项式乘积的算法。

系数表示法和点值表示法#

对于 n 次多项式 f(x)=a0+a1x+a2x2++anxn,如果我们知道了每一个 ai,那么这个多项式就唯一确定。于是我们用系数序列 a={a0,a1,a2,,an} 来表示这个多项式,这被称作系数表示法

而我们也可以取这个多项式在 n+1 个不同的 x 处的取值来表示这个多项式。根据高斯消元法,这 n+1x 以及 f(x) 的取值可以确定这个多项式。这被称作点值表示法

一个多项式从系数表示法转换为点值表示法的过程,被称作离散傅里叶变换(DFT)。反之则是离散傅里叶逆变换(IDFT)。

FFT 取一些特殊的 x,来加速 DFT 和 IDFT 的过程。这些 x 甚至不在实数域,而是一些复数。

特殊的复数#

在复平面上,一个以原点为圆心,半径为 1 的圆被称作单位圆。从实轴(x 轴)正方向开始,逆时针作 n 个向量将单位圆 n 等分,则这些向量与单位圆相交形成的 n 个交点被称作 n 次复根,第一个辐角为正的向量与单位圆的交点被称作 n单位复根,记作 ωn

image

根据复数的乘法运算法则「模长相乘,辐角相加」,可知 n 次复根的 n 次方都是 1。而所有 n 次复根都可以用 n 次单位复根的幂表示。

我们知道,2πrad=360。根据三角函数的知识,ωn 的实部即为 cos(2πn),虚部为 sin(2πn)。于是,ωn=cos(2πn)+sin(2πn)i

单位复根有一些性质:

  • ωnn=ω00=1
  • 对于 n=2mωnk=ωnk+m
  • ωnk=ω2n2k

这些奇妙的性质将会在接下来的环节中充分发挥作用。

快速傅里叶变换 / FFT#

在 FFT 中,我们将 x=ωnk(0kn1) 依次 f(x) 带入求值,便得到了一个 n1 次多项式的点值表示法。

但这样不够快,我们考虑分治。

对于 n=2k(kZ+),设 n1 次多项式

f(x)=a0+a1x+a2x2++an1xn1

=(a0+a2x2+a4x4++an2xn2)+(a1x+a3x3+a5x5+...an1xn1)

=(a0+a2x2+a4x4++an2xn2)+x(a1+a3x2+a5x4+...an1xn2)

A(x)=a0+a2x+a4x2++an2xn22

B(x)=a1+a3x+a5x2+...an1xn22

则有

f(x)=A(x2)+x×B(x2)

带入 ωnk(0k<n2)

A((ωnk)2)+ωnk×B((ωnk)2)

=A(ωn2k)+ωnk×B(ωn2k)

=A(ωn2k)+ωnk×B(ωn2k)

带入 ωnk+n2(0k<n2)

A((ωnk+n2)2)+ωnk+n2×B((ωnk+n2)2)

=A(ωn2k+n)ωnk×B(ωn2k+n)

=A(ωn2k)ωnk×B(ωn2k)

=A(ωn2k)ωnk×B(ωn2k)

因此当我们求出了 A(ωn2k),B(ωn2k) 之后,就可以求出 A(ωnk),B(ωnk)。该算法的复杂度为 T(n)=2T(n2)+O(n)=O(nlogn)

但是这样要递归,不够快!于是我们考虑优化。我们如果能求出每个系数最后到了哪个位置,就可以不断地合并这些系数,然后求解。

对于 n=8,我们来看看每次递归之后,系数是怎么被分类的:

{a0,a1,a2,a3,a4,a5,a6,a7}

{a0,a2,a4,a6},{a1,a3,a5,a7}

{a0,a4},{a2,a6},{a1,a5},{a3,a7}

{a0},{a2},{a4},{a6},{a1},{a5},{a3},{a7}

把下标用二进制表示

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

我们观察到,原来在第 i(从 0 开始)个位置的系数,最后变到了二进制翻转之后的那个位置。举个例子,a3 在现在在第 6 个位置。(3)10=(011)2,翻转之后就是 (110)2=(6)10

rii 二进制翻转之后的值。我们递推求 ri。已知 r0=0。当 i>0 时,ri2 已知。我们考虑它在二进制下与 ri 的关系:

ri2=A+0

ri=B+A

其中 An101 串,B101 串,+ 表示连接两个 01 串。

为什么是 A+0 呢?观察到 i2 的最高位一定是 0,所以翻转之后的最低位一定是 0

同时,观察到 B 是翻转后的最高位,即 i 的最低位。

于是,我们得到了 ri(i>0) 的递推式:

ri=ri22+2k1×(imod2)

其中 k 满足 FFT 的长度 n=2k

这个操作叫做蝴蝶变换,也称位逆序变换

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;
}

点值相乘#

在求出多项式 f,gn 组点值后,我们要求 h=f×gn 组点值。显然,f(i)g(i)=h(i)。于是我们对求出点值进行对位相乘的操作,就得到 hn 组点值。

下文中,我们会讲解如何通过带入的 n 组特殊点值还原出多项式本身。容易发现,对 h 进行这个过程,还原出的多项式就是 f×g

快速傅里叶逆变换 / IFFT#

我们通过 FFT 求出了 n 组点值。记它们为 y0,y1,,yn1,其中 yi=f(ωni)

设多项式

A(x)=y0+y1x+y2x2++yn1xn1

则取 x=ωni(0in1)A 进行 FFT,得到的点值序列就是原来 a 序列的 n 倍。

接下来来证明一下:

A(ωnk)=i=0n1yi(ωnk)i

=i=0n1j=0n1aj(ωni)j(ωnk)i

=j=0n1aji=0n1ωni(jk)

=j=0n1aji=0n1(ωnjk)i

后面的和式在 ωnjk=1 时值为 n,此时 n(jk),由这两个和式的范围可得 j=k

ωnjk1(此时 jk)时,i=0n1(ωnjk)i 是一个等比数列求和

i=0n1(ωnjk)i=(ωnjk)n(ωnjk)0ωnjk1

由单位复根的性质得该式值为 0

则我们继续推导,

A(ωnk)=j=0n1aji=0n1(ωnjk)i

=ak×n

于是我们只需要在 FFT 时将单位根变为 ωn1,再进行 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 国际」许可协议进行许可。

posted @   Meatherm  阅读(340)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示