快速傅里叶变换(FFT)

引入:
多项式:
A ( x ) = a 0 + a 1 x + a 2 x 2 + . . . + a n − 1 x n − 1 A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1} A(x)=a0+a1x+a2x2+...+an1xn1
B ( x ) = b 0 + b 1 x + b 2 x 2 + . . . + b m − 1 x m − 1 B(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 ( n 2 ) O(n^2) O(n2)的复杂度来计算:
多项式乘法
(平方的竖式计算)
这是因为我们用的系数表示法来存储的多项式(即把多项式的系数按顺序存在数组里)

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

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

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

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

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

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

A [ 0 ] ( x ) = a 0 + a 2 x + a 4 x 2 + . . . + a n − 2 x n / 2 − 1 A^{[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 ) = a 1 + a 3 x + a 5 x 2 + . . . + a n − 1 x n / 2 − 1 A^{[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 ] ( x 2 ) + x A [ 1 ] ( x 2 ) ∴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]
y k [ 0 ] = A [ 0 ] ( ω n 2 k ) = A [ 0 ] ( ω n / 2 k ) y^{[0]}_k=A^{[0]}(\omega_n^{2k})=A^{[0]}(\omega_{n/2}^k) yk[0]=A[0](ωn2k)=A[0](ωn/2k)
y k [ 1 ] = A [ 1 ] ( ω n 2 k ) = A [ 1 ] ( ω n / 2 k ) 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... n 2 − 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]来合成数组 y y y
因为 k &lt; n 2 k&lt;\frac n 2 k<2n,所以 y y y数组需要一半一半的求。
前半部分:
y k = A ( ω n k ) = A [ 0 ] ( ω n / 2 k ) + ω n k A [ 1 ] ( ω n / 2 k ) = y k [ 0 ] + ω n k y k [ 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]}_k yk=A(ωnk)=A[0](ωn/2k)+ωnkA[1](ωn/2k)=yk[0]+ωnkyk[1]
后半部分:
y k + n / 2 = A ( ω n k + n / 2 ) = A [ 0 ] ( ω n / 2 k + n / 2 ) + ω n k + n / 2 A [ 1 ] ( ω n / 2 k + n / 2 ) = A [ 0 ] ( ω n / 2 k ) − ω n k A [ 1 ] ( ω n / 2 k ) = y k [ 0 ] − ω n k y k [ 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]}_k 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]
分治直到长度为1,此时 y = A ( . . . ) = a 0 y=A(...)=a_0 y=A(...)=a0
然后使用上面的方法合并。

逆FFT:将点值表示法转换回系数表示法。
只需将点值表示法数组y当作系数,将 ω n − k \omega_n^{-k} ωnk代入计算,将结果除以n,即得到 a k a_k ak
a k = y 0 + y 1 ω n − k + y 2 ω n − 2 k + . . . + y n − 1 ω n − ( n − 1 ) k n a_k=\frac {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}} n ak=ny0+y1ωnk+y2ωn2k+...+yn1ωn(n1)k
又∵ y k = A ( ω n k ) = a 0 + a 1 ω n k + a 2 ω n 2 k + . . . + a n − 1 ω n ( n − 1 ) k y_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分离出 a i a_i ai单独计算:
i = k i=k i=k,∴可分离出:
a i + a i ω n i ω n − k + a i ω n 2 i ω n − 2 k + . . . + a i ω n ( n − 1 ) i ω n − ( n − 1 ) k = a i + a i + a i + . . . + a i = n a k a_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_k ai+aiωniωnk+aiωn2iωn2k+...+aiωn(n1)iωn(n1)k=ai+ai+ai+...+ai=nak
i ≠ k i≠k i̸=k,每项均可分离出:
a i + a i ω n i ω n − k + a i ω n 2 i ω n − 2 k + . . . + a i ω n ( n − 1 ) i ω n − ( n − 1 ) k a_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
= a i ω n ( i − k ) 0 + a i ω n ( i − k ) + a i ω n ( i − k ) 2 + . . . + a i ω 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)

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

综上所诉,∴分子 y 0 + y 1 ω n − k + y 2 ω n − 2 k + . . . + y n − 1 ω n − ( n − 1 ) k = n a k {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}}=na_k y0+y1ωnk+y2ωn2k+...+yn1ωn(n1)k=nak
y 0 + y 1 ω n − k + y 2 ω n − 2 k + . . . + y n − 1 ω n − ( n − 1 ) k n = a k \frac {y_0+y_1\omega_n^{-k}+y_2\omega_n^{-2k}+...+y_{n-1}\omega_n^{-(n-1)k}} n=a_k ny0+y1ωnk+y2ωn2k+...+yn1ωn(n1)k=ak

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

一个利用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
*/

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值