【数学】FFT蒟蒻的研究历程

本文深入探讨了快速傅里叶变换(FFT)的原理及其在多项式乘法中的应用,详细解释了如何通过FFT将时间复杂度从O(n^2)降低到O(nlog_2(n)),并提供了具体的实现代码。

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

本来很久以前就学了,但是我一直没有写博客,直到最近要用的时候才发现忘了。于是就有了这篇博客.
我们知道,如果直接暴力将两个多项式比如
A(x)=a0+a1x+a2x2+...+anxnA(x)=a_0+a_1x+a_2x^2+...+a_nx^nA(x)=a0+a1x+a2x2+...+anxn
B(x)=b0+b1x+b2x2+...+bnxnB(x)=b_0+b_1x+b_2x^2+...+b_nx^nB(x)=b0+b1x+b2x2+...+bnxn
相乘将会花费O(n2)O(n^2)O(n2)的时间,有没有更快的方法呢?
我们注意到,耗时的原因是每一项都要花费O(n)O(n)O(n)的时间来乘,合起来乘的难度较大,说不定换一种表示方法就可以不一样了。
我们要确定一个有nnn次项的方程,不仅可以用上面的方法,我们也可以用nnn个点(向量)来表示,而且这样一来方程就可以唯一确定了。
很明显如果是用nnn个点来表示A(x)A(x)A(x)的话,A(x),B(x)A(x),B(x)A(x),B(x)可以表示为:
(x1,A(x1)),(x2,A(x2)),...,(xn,A(xn))(x_1,A(x_1)),(x_2,A(x_2)),...,(x_n,A(x_n))(x1,A(x1)),(x2,A(x2)),...,(xn,A(xn))
(x1,B(x1)),(x2,B(x2)),...,(xn,B(xn))(x_1,B(x_1)),(x_2,B(x_2)),...,(x_n,B(x_n))(x1,B(x1)),(x2,B(x2)),...,(xn,B(xn))
由于我们要求的C(x)=A(x)∗B(x)C(x)=A(x)*B(x)C(x)=A(x)B(x)因此C(x)C(x)C(x)可以表示为:
(x1,A(x1)∗B(x1)),(x2,A(x2)∗B(x2)),...,(xn,A(xn)∗B(xn))(x_1,A(x_1)*B(x_1)),(x_2,A(x_2)*B(x_2)),...,(x_n,A(x_n)*B(x_n))(x1,A(x1)B(x1)),(x2,A(x2)B(x2)),...,(xn,A(xn)B(xn))
在这种表示方法下我们只需要O(n)O(n)O(n)时间就可以将两个方程相乘,但是我们已有的是A(x)A(x)A(x)B(x)B(x)B(x)的系数表示法的式子。
但如果暴力转成点值表示法,需要O(n2)O(n^2)O(n2)的时间,时间复杂度并没有丝毫的减少,而且常数反而变大了。我们需要找到更快的能在系数表示法和点值表示法之间转换的方法。
很明显,我们在带入n个值求点是在做操作重复的运算,而且由于这n个值的取值我们现在还没有确定,这里应该可以通过巧妙的取值来达到减少运算的目的,要是我们选的数的次方之间可以相互轮回,那么应该可以简化运算。一维的实数相乘的话只能在数轴上面走,二维的虚数就比较方便了,因为(cos(a)+isin(a))n=(cos(na)+isin(na))(cos(a)+isin(a))^n=(cos(na)+isin(na))(cos(a)+isin(a))n=(cos(na)+isin(na))我们如果用这样的数,那么就它的n次方就是相当于在平面上转圈,如下图:
在这里插入图片描述
因此如上图(w81)n=w8n(w_8^1)^n=w_8^n(w81)n=w8n.
我们可以发现它有这些性质:
w2n2m=wnmw_{2n}^{2m}=w_n^mw2n2m=wnm
wnm+n2=−wnmw_n^{m+\frac{n}{2}}=-w_n^mwnm+2n=wnm
因此各次方的关系就有了,我们可以把最后的式子排在一起看看
比如现在只有4个:
a0+a1∗w40+a2∗(w40)2+a3∗(w40)3a_0+a_1*w_4^0+a_2*(w_4^0)^2+a_3*(w_4^0)^3a0+a1w40+a2(w40)2+a3(w40)3
a0+a1∗w41+a2∗(w41)2+a3∗(w41)3a_0+a_1*w_4^1+a_2*(w_4^1)^2+a_3*(w_4^1)^3a0+a1w41+a2(w41)2+a3(w41)3
a0+a1∗w42+a2∗(w42)2+a3∗(w42)3a_0+a_1*w_4^2+a_2*(w_4^2)^2+a_3*(w_4^2)^3a0+a1w42+a2(w42)2+a3(w42)3
a0+a1∗w43+a2∗(w43)2+a3∗(w43)3a_0+a_1*w_4^3+a_2*(w_4^3)^2+a_3*(w_4^3)^3a0+a1w43+a2(w43)2+a3(w43)3
即:
a0+a1∗w40+a2∗w40+a3∗w40a_0+a_1*w_4^0+a_2*w_4^0+a_3*w_4^0a0+a1w40+a2w40+a3w40
a0+a1∗w41+a2∗w42+a3∗w43a_0+a_1*w_4^1+a_2*w_4^2+a_3*w_4^3a0+a1w41+a2w42+a3w43
a0+a1∗w42+a2∗w44+a3∗w46a_0+a_1*w_4^2+a_2*w_4^4+a_3*w_4^6a0+a1w42+a2w44+a3w46
a0+a1∗w43+a2∗w46+a3∗w49a_0+a_1*w_4^3+a_2*w_4^6+a_3*w_4^9a0+a1w43+a2w46+a3w49
由于wnm+n=wnmw_n^{m+n}=w_n^mwnm+n=wnm,我们可以把次数模n加强联系即:
a0+a1∗w40+a2∗w40+a3∗w40a_0+a_1*w_4^0+a_2*w_4^0+a_3*w_4^0a0+a1w40+a2w40+a3w40
a0+a1∗w41+a2∗w42+a3∗w43a_0+a_1*w_4^1+a_2*w_4^2+a_3*w_4^3a0+a1w41+a2w42+a3w43
a0+a1∗w42+a2∗w40+a3∗w42a_0+a_1*w_4^2+a_2*w_4^0+a_3*w_4^2a0+a1w42+a2w40+a3w42
a0+a1∗w43+a2∗w42+a3∗w41a_0+a_1*w_4^3+a_2*w_4^2+a_3*w_4^1a0+a1w43+a2w42+a3w41
由于w42=−w40,w43=−w41w_4^2=-w_4^0,w_4^3=-w_4^1w42=w40,w43=w41我们可以进一步把次数降低加强联系即:
a0+a1∗w40+a2∗w40+a3∗w40a_0+a_1*w_4^0+a_2*w_4^0+a_3*w_4^0a0+a1w40+a2w40+a3w40
a0+a1∗w41−a2∗w40−a3∗w41a_0+a_1*w_4^1-a_2*w_4^0-a_3*w_4^1a0+a1w41a2w40a3w41
a0−a1∗w40+a2∗w40−a3∗w40a_0-a_1*w_4^0+a_2*w_4^0-a_3*w_4^0a0a1w40+a2w40a3w40
a0−a1∗w41−a2∗w40+a3∗w41a_0-a_1*w_4^1-a_2*w_4^0+a_3*w_4^1a0a1w41a2w40+a3w41
可以发现其中的(0,2)(1,3)式的次数是完全一样的,系数的符号有一半一样,另一半互为相反数,其实这很好证明第mmm式和第m+n2m+\frac {n}{2}m+2n式的关系。
fm=a0+a1∗wnm+a2∗wn2m+...+an−1∗wn(n−1)∗mf_m=a_0+a_1*w_n^m+a_2*w_n^{2m}+...+a_{n-1}*w_n^{(n-1)*m}fm=a0+a1wnm+a2wn2m+...+an1wn(n1)m
fm+n2=a0+a1∗wnm+n2+a2∗wn2m+...+an−1∗wn(n−1)∗(m+n2)f_{m+\frac{n}{2}}=a_0+a_1*w_n^{m+\frac{n}{2}}+a_2*w_n^{2m}+...+a_{n-1}*w_n^{(n-1)*(m+\frac{n}{2})}fm+2n=a0+a1wnm+2n+a2wn2m+...+an1wn(n1)(m+2n)

fm+n2=a0−a1∗wnm+a2∗wn2m+...−an−1∗wn(n−1)∗mf_{m+\frac{n}{2}}=a_0-a_1*w_n^m+a_2*w_n^{2m}+...-a_{n-1}*w_n^{(n-1)*m}fm+2n=a0a1wnm+a2wn2m+...an1wn(n1)m(假设n是偶数)
那么我们可以把这之间相同的部分拿出来

A0=a0+a2∗wn2m+a4∗wn4m+...+an−2∗wn(n−2)∗mA_0=a_0+a_2*w_n^{2m}+a_4*w_n^{4m}+...+a_{n-2}*w_n^{(n-2)*m}A0=a0+a2wn2m+a4wn4m+...+an2wn(n2)m
A1=a1+a3∗wn2m+a5∗wn4m+...+an−1∗wn(n−2)∗mA_1=a_1+a_3*w_n^{2m}+a_5*w_n^{4m}+...+a_{n-1}*w_n^{(n-2)*m}A1=a1+a3wn2m+a5wn4m+...+an1wn(n2)m

A0=a0+a2∗wn2m+a4∗wn22m+...+an−2∗wn2(n−2)∗m/2A_0=a_0+a_2*w_{\frac{n}{2}}^{m}+a_4*w_{\frac{n}{2}}^{2m}+...+a_{n-2}*w_{\frac{n}{2}}^{(n-2)*m/2}A0=a0+a2w2nm+a4w2n2m+...+an2w2n(n2)m/2
A1=a1+a3∗wn2m+a5∗wn22m+...+an−1∗wn2(n−2)∗m/2A_1=a_1+a_3*w_{\frac{n}{2}}^{m}+a_5*w_{\frac{n}{2}}^{2m}+...+a_{n-1}*w_{\frac{n}{2}}^{(n-2)*m/2}A1=a1+a3w2nm+a5w2n2m+...+an1w2n(n2)m/2
那么很明显
fm=A0+A1∗wnmf_m=A_0+A_1*w_n^mfm=A0+A1wnm
fm+n2=A0+A1∗wnm+n2=A0−A1∗wnmf_{m+\frac{n}{2}}=A_0+A_1*w_n^{m+\frac{n}{2}}=A_0-A_1*w_n^mfm+2n=A0+A1wnm+2n=A0A1wnm
因此我们只要把fmf_mfmfm+n2f_{m+\frac{n}{2}}fm+2n求出来就可以一下求出来A0A_0A0A1A_1A1 而且fff的数据范围只有AAA的一半。也就是说只要log2(n)log_2(n)log2(n)层运算就可以求出解了,说以总的时间复杂度只有nlog2(n)nlog_2(n)nlog2(n).

a0+a1w40+a2w40+a3w40a_0+a_1w_4^0+a_2w_4^0+a_3w_4^0a0+a1w40+a2w40+a3w40a0+a1w41+a2w42+a3w43a_0+a_1w_4^1+a_2w_4^2+a_3w_4^3a0+a1w41+a2w42+a3w43a0+a1w42+a2w44+a3w46a_0+a_1w_4^2+a_2w_4^4+a_3w_4^6a0+a1w42+a2w44+a3w46a0+a1w43+a2w46+a3w49a_0+a_1w_4^3+a_2w_4^6+a_3w_4^9a0+a1w43+a2w46+a3w49
a0+a2w20a_0+a_2w_2^0a0+a2w20a0+a2w21a_0+a_2w_2^1a0+a2w21a1+a3w20a_1+a_3w_2^0a1+a3w20a1+a3w21a_1+a_3w_2^1a1+a3w21
a0a_0a0a2a_2a2a1a_1a1a3a_3a3

为了方便处理我们将对应位置的f对着对应位置的A,表示成数字就是:
0 1 2 3 4 5 6 7
0 2 4 6 1 3 5 7
0 4 2 6 1 5 3 7
不难发现之后得到的位置的二进制就是按反过来之后排序,因为我们第一次把&1=0的移动到了前面,而吧&1=1的移到了后面,所以之后第二次把&2=0的移动到了前面,而吧&2=1的移到了后面,之后依次这样就可以了。这就是蝴蝶变换。
因此我们在实际操作的时候就反着来,从下至上依次求出每一层,这里不递归的原因是因为递归太慢了。
说以我们现在可以用O(nlog2(n))O(nlog_2(n))O(nlog2(n))的时间复杂度正着把系数表示法变成点值表示法,那么怎么倒着装呢?
很明显为了求出上面的值,我们采取的方法是倒着来,既然
fm=A0+A1∗wnmf_m=A_0+A_1*w_n^mfm=A0+A1wnm
fm+n2=A0−A1∗wnmf_{m+\frac{n}{2}}=A_0-A_1*w_n^mfm+2n=A0A1wnm
那么
2∗A0=fm+fm+n22*A_0=f_m+f_{m+\frac{n}{2}}2A0=fm+fm+2n
2∗A1=(fm−fm+n2)∗wn−m2*A_1=(f_m-f_{m+\frac{n}{2}})*w_n^{-m}2A1=(fmfm+2n)wnm
我们先暴力把最终得到的式子求出来再统一除n(n=2某次方)n(n=2^{某次方})n(n=2),得到:

4∗a04*a_04a04∗a14*a_14a14∗a24*a_24a24∗a34*a_34a3
2∗(a0+a2w40)2*(a_0+a_2w_4^0)2(a0+a2w40)2∗(a1+a3w40)2*(a_1+a_3w_4^0)2(a1+a3w40)2∗(a0+a2w42)2*(a_0+a_2w_4^2)2(a0+a2w42)2∗(a1+a3w42)2*(a_1+a_3w_4^2)2(a1+a3w42)
a0+a1w40+a2w40+a3w40a_0+a_1w_4^0+a_2w_4^0+a_3w_4^0a0+a1w40+a2w40+a3w40a0+a1w42+a2w44+a3w46a_0+a_1w_4^2+a_2w_4^4+a_3w_4^6a0+a1w42+a2w44+a3w46a0+a1w41+a2w42+a3w43a_0+a_1w_4^1+a_2w_4^2+a_3w_4^3a0+a1w41+a2w42+a3w43a0+a1w43+a2w46+a3w49a_0+a_1w_4^3+a_2w_4^6+a_3w_4^9a0+a1w43+a2w46+a3w49

比如这里n就是4,FFT只能解决n是二的整次幂的情况,如果开始的n不是2的整次幂那么加一堆系数为0的项就可以了,反正加的项又不到一倍,时间也多不了多少。
这样式子就可以反过来了
不过貌似再写逆变换有点麻烦,大佬们搞了个逆矩阵至今没搞懂,留坑了。
这里是代码(洛谷3803)

#include<cstdio>
#include<complex>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=4000005;
typedef complex<double> cpx;
const long double PI=acos(0.0)*2.0;
int N,M;
void FFT(cpx a[],int n,double op)
{
	for(int i=1,j=0;i<n-1;i++)
	{
		for(int s=n;j^=(s>>=1),(~j)&s;);
		if(i<j)
			swap(a[i],a[j]);
	}
	for(int len=2,mid=1;len<=n;len<<=1,mid<<=1)
	{
		cpx wm(cos(2.0*PI/len),op*sin(2.0*PI/len));
		for(int now=0;now<n;now+=len)
		{
			cpx w(1,0);
			for(int k=now;k<now+mid;k++,w=w*wm)
			{
				cpx l=a[k],r=w*a[k+mid];
				a[k]=l+r,a[k+mid]=l-r;
			}
		}
	}
	if(op==-1)
	{
		for(int i=0;i<n;i++)
			a[i]/=n;
	}
}
cpx A[MAXN],B[MAXN];
void mul(int a[],int n,int b[],int m,int c[])
{
	//memset(A,0,sizeof(A));
	//memset(B,0,sizeof(B));
	for(int i=0;i<n;i++)
		A[i]=cpx(a[i],0);
	for(int i=0;i<m;i++)
		B[i]=cpx(b[i],0);
	int l=n+m-1,len=1;
	while(len<l)
		len<<=1;
	FFT(A,len,1);
	FFT(B,len,1);
	for(int i=0;i<len;i++)
		A[i]=A[i]*B[i];
	FFT(A,len,-1);
	for(int i=0;i<len;i++)
		c[i]=int(A[i].real()+0.5);
}
int F[MAXN],G[MAXN],an[MAXN];
int main()
{
	scanf("%d %d",&N,&M);
	N++,M++;
	for(int i=0;i<N;i++)
		scanf("%d",&F[i]);
	for(int i=0;i<M;i++)
		scanf("%d",&G[i]);
	mul(F,N,G,M,an);
	for(int i=0;i<N+M-1;i++)
		printf("%d%c",an[i],i==N+M-2?'\n':' ');
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值