快速傅里叶变换(FFT)

本文介绍了一种高效计算多项式乘法的方法,通过快速傅里叶变换(FFT)实现从系数表示法到点值表示法的转换,从而大幅度降低计算复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引入:
多项式:
A(x)=a0+a1x+a2x2+...+an−1xn−1A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}A(x)=a0+a1x+a2x2+...+an1xn1
B(x)=b0+b1x+b2x2+...+bm−1xm−1B(x)=b_0+b_1x+b_2x^2+...+b_{m-1}x^{m-1}B(x)=b0+b1x+b2x2+...+bm1xm1
在计算多项式的乘法,如A(x)×B(x)A(x)×B(x)A(x)×B(x)时,往往需要O(n2)O(n^2)O(n2)的复杂度来计算:
多项式乘法
(平方的竖式计算)
这是因为我们用的系数表示法来存储的多项式(即把多项式的系数按顺序存在数组里)

然而,有一种更好的表示方法——点值表示法:给一个多项式带入不同的x值,得到不同的y值(如同一个函数图像,从上面取n个点的坐标)
yk=A(xk)(所有xk不相同,k=0,1,2,...,n−1)y_k=A(x_k)(所有x_k不相同,k=0,1,2,...,n-1)yk=A(xk)(xkk=0,1,2,...,n1)
这样,我们可以固定一串xkx_kxk,用数组存储一串yky_kyk来表示多项式。那么两个多项式相乘A(xk)×B(xk)=Ayk×BykA(x_k)×B(x_k)=A_{yk}×B_{yk}A(xk)×B(xk)=Ayk×Byk,只需要将数组对应乘起来就可以了,O(n)O(n)O(n)

然而又有问题了,好像没有人也没有题读多项式用点值表示法吧。。。我们需要把系数表示法转换为点值表示法,乘了过后,又转换回去。
如何转换?(数学老师:把点带进函数,解方程。。。好像又变回O(n2)O(n^2)O(n2)了,那刚刚那个O(n)O(n)O(n)乘法并没有卵用)

又有一个新的圣器——快速傅里叶变换
选用一个神奇的叫单位复根的东西来作为xkx_kxk,用分治的思想O(nlogn)O(nlogn)O(nlogn)完成转换。

单位复根
nnn次单位复根即n次方为1的数,ωn=1\omega^n=1ωn=1的复数ω\omegaω,而n次单位复根恰好有n个,分别是e2πik/ne^{2\pi ik/n}e2πik/n(k=0,1,...,n−1)(k=0,1,...,n-1)(k=0,1,...,n1)
又∵eiθ=cosθ+i sinθe^{i\theta}=cos\theta+i\ sin\thetaeiθ=cosθ+i sinθ
∴n个n次单位复根就会像这样呈现↓
复平面上的8个8次单位复根
ωab\omega_a^bωab就是一个半径为1的圆,转ba\frac b aab圈的位置。
这样,单位复根就有了一些神奇的数学性质:

  • ωdndk=ωnk\omega_{dn}^{dk}=\omega_n^kωdndk=ωnk,证明很简单,转到dkdn\frac {dk}{dn}dndk圈就等于kn\frac k nnk
  • n>0n>0n>0且n为偶数,则n个n次单位复根的平方等于n/2个n/2次单位复根。
    证明:
    k≤n2k≤\frac n 2k2n
    (ωnk)2=ωn2k=ω2n/22k=ωn/2k(\omega_n^k)^2=\omega_n^{2k}=\omega_{2n/2}^{2k}=\omega_{n/2}^k(ωnk)2=ωn2k=ω2n/22k=ωn/2k
    (ωnn/2+k)2=ωnn+2k=ωnnωn2k=(ωnk)2(\omega_n^{n/2+k})^2=\omega_n^{n+2k}=\omega_n^n\omega_n^{2k}=(\omega_n^k)^2(ωnn/2+k)2=ωnn+2k=ωnnωn2k=(ωnk)2(同上) (∵ωnn=1\omega_n^n=1ωnn=1
  • 对于任意整数n≥1n≥1n1kkk不为nnn的倍数,即ωnk≠1\omega_n^k≠1ωnk̸=1,则Σjn−1(ωnk)j=0\Sigma^{n-1}_j(\omega_n^k)^j=0Σjn1(ωnk)j=0
    证明:
    这为等比数列求和,利用公式↓
    Σjn−1(ωnk)j=(ωnk)n−1ωnk−1=1k−1ωnk−1=0\Sigma^{n-1}_j(\omega_n^k)^j=\frac {(\omega_n^k)^n-1} {\omega_n^k -1}=\frac {1^k-1} {\omega_n^k-1}=0Σjn1(ωnk)j=ωnk1(ωnk)n1=ωnk11k1=0
  • ωnk+n/2=−ωnk\omega_n^{k+n/2}=-\omega_n^kωnk+n/2=ωnk,相当于再kn\frac k nnk处再转半圈,正好转到对面

FFT之Cooley-Tukey算法:分治思想。
为保证分治成功,需要将n扩大到最近的2的幂。
我们要计算A(x)=a0+a1x+a2x2+...+an−1xn−1A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}A(x)=a0+a1x+a2x2+...+an1xn1
的点值表示法数组y
yi=A(ωni)y_i=A(\omega_n^i)yi=A(ωni)

A[0](x)=a0+a2x+a4x2+...+an−2xn/2−1A^{[0]}(x)=a_0+a_2x+a_4x^2+...+a_{n-2}x^{n/2-1}A[0](x)=a0+a2x+a4x2+...+an2xn/21
A[1](x)=a1+a3x+a5x2+...+an−1xn/2−1A^{[1]}(x)=a_1+a_3x+a_5x^2+...+a_{n-1}x^{n/2-1}A[1](x)=a1+a3x+a5x2+...+an1xn/21
∴A(x)=A[0](x2)+xA[1](x2)∴A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2)A(x)=A[0](x2)+xA[1](x2)
所以我们可以先分治求解数组y[0]y^{[0]}y[0]y[1]y^{[1]}y[1]
yk[0]=A[0](ωn2k)=A[0](ωn/2k)y^{[0]}_k=A^{[0]}(\omega_n^{2k})=A^{[0]}(\omega_{n/2}^k)yk[0]=A[0](ωn2k)=A[0](ωn/2k)
yk[1]=A[1](ωn2k)=A[1](ωn/2k)y^{[1]}_k=A^{[1]}(\omega_n^{2k})=A^{[1]}(\omega_{n/2}^k)yk[1]=A[1](ωn2k)=A[1](ωn/2k)
(k=0,1,2...n2−1)(k=0,1,2...\frac n 2 -1)(k=0,1,2...2n1)
规模正好缩小一半。
然后我们需要用y[0]y^{[0]}y[0]y[1]y^{[1]}y[1]来合成数组yyy
因为k&lt;n2k&lt;\frac n 2k<2n,所以yyy数组需要一半一半的求。
前半部分:
yk=A(ωnk)=A[0](ωn/2k)+ωnkA[1](ωn/2k)=yk[0]+ωnkyk[1]y_k=A(\omega_n^k)=A^{[0]}(\omega_{n/2}^k)+\omega_n^kA^{[1]}(\omega_{n/2}^k)=y^{[0]}_k+\omega_n^ky^{[1]}_kyk=A(ωnk)=A[0](ωn/2k)+ωnkA[1](ωn/2k)=yk[0]+ωnkyk[1]
后半部分:
yk+n/2=A(ωnk+n/2)=A[0](ωn/2k+n/2)+ωnk+n/2A[1](ωn/2k+n/2)=A[0](ωn/2k)−ωnkA[1](ωn/2k)=yk[0]−ωnkyk[1]y_{k+n/2}=A(\omega_n^{k+n/2})=A^{[0]}(\omega_{n/2}^{k+n/2})+\omega_n^{k+n/2}A^{[1]}(\omega_{n/2}^{k+n/2})=A^{[0]}(\omega_{n/2}^k)-\omega_n^kA^{[1]}(\omega_{n/2}^k)=y^{[0]}_k-\omega_n^ky^{[1]}_kyk+n/2=A(ωnk+n/2)=A[0](ωn/2k+n/2)+ωnk+n/2A[1](ωn/2k+n/2)=A[0](ωn/2k)ωnkA[1](ωn/2k)=yk[0]ωnkyk[1]
分治直到长度为1,此时y=A(...)=a0y=A(...)=a_0y=A(...)=a0
然后使用上面的方法合并。

逆FFT:将点值表示法转换回系数表示法。
只需将点值表示法数组y当作系数,将ωn−k\omega_n^{-k}ωnk代入计算,将结果除以n,即得到aka_kak
ak=y0+y1ωn−k+y2ωn−2k+...+yn−1ωn−(n−1)kna_k=\frac {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}} nak=ny0+y1ωnk+y2ωn2k+...+yn1ωn(n1)k
又∵yk=A(ωnk)=a0+a1ωnk+a2ωn2k+...+an−1ωn(n−1)ky_k=A(\omega_n^k)={a_0+a_1\omega_n^{k}+a_2\omega_n^{2k}+...+a_{n-1}\omega_n^{(n-1)k}}yk=A(ωnk)=a0+a1ωnk+a2ωn2k+...+an1ωn(n1)k
∴在上面分子中所有y分离出aia_iai单独计算:
i=ki=ki=k,∴可分离出:
ai+aiωniωn−k+aiωn2iωn−2k+...+aiωn(n−1)iωn−(n−1)k=ai+ai+ai+...+ai=naka_i+a_i\omega_n^{i}\omega_n^{-k}+a_i\omega_n^{2i}\omega_n^{-2k}+...+a_i\omega_n^{(n-1)i}\omega_n^{-(n-1)k}=a_i+a_i+a_i+...+a_i=na_kai+aiωniωnk+aiωn2iωn2k+...+aiωn(n1)iωn(n1)k=ai+ai+ai+...+ai=nak
i≠ki≠ki̸=k,每项均可分离出:
ai+aiωniωn−k+aiωn2iωn−2k+...+aiωn(n−1)iωn−(n−1)ka_i+a_i\omega_n^{i}\omega_n^{-k}+a_i\omega_n^{2i}\omega_n^{-2k}+...+a_i\omega_n^{(n-1)i}\omega_n^{-(n-1)k}ai+aiωniωnk+aiωn2iωn2k+...+aiωn(n1)iωn(n1)k
=aiωn(i−k)0+aiωn(i−k)+aiωn(i−k)2+...+aiωn(i−k)(n−1)=a_i\omega_n^{(i-k)0}+a_i\omega_n^{(i-k)}+a_i\omega_n^{(i-k)2}+...+a_i\omega_n^{(i-k)(n-1)}=aiωn(ik)0+aiωn(ik)+aiωn(ik)2+...+aiωn(ik)(n1)

=aiΣjn−1(ωn(i−k))j=0=a_i\Sigma^{n-1}_j(\omega_n^{(i-k)})^j=0=aiΣjn1(ωn(ik))j=0

综上所诉,∴分子y0+y1ωn−k+y2ωn−2k+...+yn−1ωn−(n−1)k=nak{y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}}=na_ky0+y1ωnk+y2ωn2k+...+yn1ωn(n1)k=nak
y0+y1ωn−k+y2ωn−2k+...+yn−1ωn−(n−1)kn=ak\frac {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}} n=a_kny0+y1ωnk+y2ωn2k+...+yn1ωn(n1)k=ak

逆FFT做法就是:将点值表示法数组y当作系数,将ωn−k\omega_n^{-k}ωnk代入计算,将结果除以n,即得到aka_kak

一个利用FFT做多项式乘积的程序。
代码:(递归版本)

#include<cstdio>
#include<cmath>
#include<cstring>
#include<complex>
using namespace std;
const int MAXN=1005;
typedef complex<double> cpxd;
cpxd out[MAXN];
void FFT(cpxd *A,cpxd *out,int n,int flag,int step=1)
/*
A是原数列
out是返回的点值表示法
n是长度
flag=1 FFT;flag=-1 逆FFT
step表示相邻两个数的距离。
因为FFT不停的提出奇偶项递归,则此时递归的数列在原数列A中,相邻两个数的距离为step
比如1,2,3,4,5,6,7,8 -> 1,3,5,7 -> 1,5  (这两个数在原数列中距离为4)

*/
{
	if(n<1)return;
	if(n==1)
	{out[0]=A[0];return;}
	FFT(A,out,n/2,flag,step*2);
	FFT(A+step,out+n/2,n/2,flag,step*2);
	for(int i=0;i<n/2;i++)
	{
		cpxd temp=exp(cpxd(0,flag*acos(-1)*2.0*i/n))*out[n/2+i];
		//exp那一坨生成单位复根
		out[n/2+i]=out[i]-temp;
		out[i]=out[i]+temp;
	}
	if(flag==-1&&step==1)
		for(int i=0;i<n;i++)
			out[i]/=n;
}
void mul(cpxd *A,cpxd *B,cpxd *C,int n)
{
	int len=1;
	n<<=1;
	while(len<n)len<<=1;
	FFT(A,out,len,1);
	memcpy(A,out,sizeof(cpxd)*len);
	FFT(B,out,len,1);
	memcpy(B,out,sizeof(cpxd)*len);
	for(int i=0;i<len;i++)
		C[i]=A[i]*B[i];
	FFT(C,out,len,-1);
	memcpy(C,out,sizeof(cpxd)*len);
}
int main()
{
	int n;
	scanf("%d",&n);
	cpxd a[MAXN],b[MAXN],c[MAXN];
	double x;
	for(int i=1;i<=n;i++)
	{
		scanf("%lf",&x);
		a[i]=cpxd(x,0.0);
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%lf",&x);
		b[i]=cpxd(x,0.0);
	}
	mul(a+1,b+1,c+1,n);
	for(int i=1;i<2*n-1;i++)
		printf("%lf ",c[i].real());
	printf("%lf\n",c[2*n-1].real());
	return 0;
}
/*
3
1 2 3
3 2 1
*/
/*
3.000000 8.000000 14.000000 8.000000 3.000000
*/

再来改为非递归版本,速度更快
看看递归版本如何迭代的:
FFT递归迭代树状图
如果我们把初始的数组排成最后的样子,就不需要递归了。
观察最后的数字的二进制
0 4 2 6 1 5 3 7
000 100 010 110 001 101 011 111
正好是把数字倒过来按顺序排列

代码:(非递归版本)

#include<cstdio>
#include<cmath>
#include<complex>
using namespace std;
const int MAXN=1005;
typedef complex<double> cpxd;
void FFT(cpxd *A,int n,int flag)
{
	for(int i=0,j=0;i<n;i++)
	{
		if(j>i)swap(A[i],A[j]);//排序
		int k=n;
		while(j&(k>>=1))
			j^=k;
		j|=k;
		//把二进制数字倒过来后,生成下一个数
	}
	double pi=flag*acos(0)*2.0;
	for(int i=1;i<n;i<<=1)//每次递归的长度的一半
	{
		double tmp=pi/i;//因为i为长度的一半,所以÷i就已经乘以了2
		for(int j=0;j<i;j++)
		{
			cpxd w=exp(cpxd(0,tmp*j));//生成单位复根
			for(int l=j,r;l<n;l+=(i<<1))//计算每一组长度为2i的数列,一次合并
			{
				r=l+i;
				cpxd temp=w*A[r];
				A[r]=A[l]-temp;
				A[l]=A[l]+temp;
			}
		}
	}
	if(flag==-1)
		for(int i=0;i<n;i++)
			A[i]/=n;
}
void mul(cpxd *A,cpxd *B,cpxd *C,int n)
{
	int len=1;
	n<<=1;
	while(len<n)len<<=1;
	FFT(A,len,1);
	FFT(B,len,1);
	for(int i=0;i<len;i++)
		C[i]=A[i]*B[i];
	FFT(C,len,-1);
}
int main()
{
	int n;
	scanf("%d",&n);
	cpxd a[MAXN],b[MAXN],c[MAXN];
	double x;
	for(int i=1;i<=n;i++)
	{
		scanf("%lf",&x);
		a[i]=cpxd(x,0.0);
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%lf",&x);
		b[i]=cpxd(x,0.0);
	}
	mul(a+1,b+1,c+1,n);
	for(int i=1;i<2*n-1;i++)
		printf("%lf ",c[i].real());
	printf("%lf\n",c[2*n-1].real());
	return 0;
}
/*
3
1玩
2 3
3 2 1
*/
/*
3.000000 8.000000 14.000000 8.000000 3.000000
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CaptainHarryChen

随便

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值