算法学习:快速傅里叶变换(FFT)

版权声明:原创文章,转载要注明作者哦 https://blog.csdn.net/DYT_B/article/details/84672286

前置知识


1.多项式

定义

形如:
f ( x ) = 0 n 1 a i x i f(x)=\sum_{0}^{n-1}ai\cdot x^i

多项式表示法:

系数表示法

就是上式的写法

点值表示法

在f(x)上取n个点,就能唯一确定的表示出这个多项式。
证明如下:
\forall n点集合c
定义集合A={ a 0 , a 1 , a 2 , . . . , a n 1 a_0,a_1,a_2,...,a_{n-1} }为某一个的n点的集合c所表示的多项式的系数集合。
反证:
若不然,则 \exists 集合B,B ̸ = \not = A,并且B也是c所表示多项式的系数集合。
此时,令Ci=Ai-Bi,那么带入c中的每一个元素,C所代表的多项式的值都会是0,也就是说,该n-1次多项式有n个零点,这显然与事实不符,所以证毕。

多项式相乘

如果用系数表示法直接相乘,那么计算的复杂度是O( n 2 n^{2} )的。
但是如果用点值表示法相乘,由于函数的相乘是每一个点的两个函数值相乘,所以复杂度是O(n)的。

2.复数相关

1.复数定义

在数学上,我们定义i,满足 i 2 = 1 i^2=-1 也就是说 i = 1 i=\sqrt{-1}
定义一个复数为 a + b i a+bi 那么由此我们就把数的定义扩充到了更大的范围。

2.复平面

复数的定义 a + b i a+bi ,我们可以把a看成x轴,b看成y轴,那么每一个复数都可以看做在复平面上的一个点。当然,原本的实数就是复平面中x轴上的点。

3.复平面上的运算法则

复数的运算法则: ( a 1 + b 1 i ) ( a 2 + b 2 i ) = (a_1+b_1i)\cdot(a_2+b_2i)=
a 1 b 1 + a 1 b 2 i + a 2 b 1 i + b 1 b 2 i 2 = a_1b_1+a_1b_2i+a_2b_1i+b_1b_2i^2=
a 1 b 1 b 1 b 2 + ( a 1 b 2 + a 2 b 1 ) i a_1b_1-b_1b_2+(a_1b_2+a_2b_1)i

复平面上的复数可以看做向量,那么复平面上的复数可以表示为(辐角(与x轴的夹角大小),长度)。对于这种表示方法,向量的相加的法则是:长度相乘,辐角相加。
证明:
假设两个复数 a 1 + b 1 i , a 2 + b 2 i a_1+b_1i,a_2+b_2i
那么两个复数的长度分别为 a 1 2 + b 1 2 , a 2 2 + b 2 2 \sqrt{{a_1}^2+{b_1}^{2}},\sqrt{{a_2}^2+{b_2}^{2}}
相乘等于
a 1 2 a 2 2 + a 1 2 b 2 2 + b 1 2 a 2 2 + b 1 2 b 2 2 \sqrt{{a_1}^2{a_2}^2+{a_1}^2{b_2}^2+{b_1}^2{a_2}^2+{b_1}^2{b_2}^2}
而两个向量相乘后得到的向量 a 1 b 1 b 1 b 2 + ( a 1 b 2 + a 2 b 1 ) i a_1b_1-b_1b_2+(a_1b_2+a_2b_1)i 的长度也等于上式的值。
辐角相加也类似可证,只不过需要用到定理 tan a + b = tan a + tan b 1 tan a tan b \tan a+b=\frac{\tan a+\tan b}{1-\tan a\tan b} ,这里就省略了。

单位根

定义

定义 ω n \omega_n 为在复平面上,以1为半径作圆,把圆等分成n份。
那么 ω n k \omega_n^k 就表示这个n等分圆中,从x轴开始,逆时针数的第k条等分线所表示的复数。
我们规定x轴上的等分线为第0条,同时也是的n条,也就是说 ω n 0 = ω n n = 1 \omega_n^0=\omega_n^n=1 ,意思就是等分从x轴开始。
那么有一个显然的计算 ω n k \omega_n^k 的方法: ω n k = cos k 2 π n + sin k 2 π n i \omega_n^k=\cos k\frac{2\pi}{n}+\sin k\frac{2\pi}{n}\cdot i

性质
性质1

ω 2 n 2 k = ω n k \omega_{2n}^{2k}=\omega_n^k
这个可以感性理解一下,一个圆分成n份,取第k份,和一个圆分成2n份,取第2k份,实际上是相同的。

性质2

ω n k + n 2 = ω n k \omega_{n}^{k+\frac{n}{2}}=-\omega_n^k
这个性质也很好理解,画一个图即可。


FFT

FFT是实现傅里叶变换(DFT)和离散傅里叶变换(IDFT)的方法

1.离散傅里叶变换(DFT)

DFT实际上是将多项式由系数表示法变为点值表示法的过程。
假设有一个n次的多项式
如果用暴力的方法实现这一过程,那么就是随意取n个点,分别带入多项式,求出此时的值,这样做的复杂度是O( n 2 n^2 )的。
那么我们考虑特殊的取点, ω n 0 , . . . , ω n n 1 \omega_n^0,...,\omega_n^{n-1} ,带入这些点,会对我们的转变效率有什么提高呢?
我们考虑一个多项式F
F ( x ) = a 0 + a 1 x + a 2 x 2 + . . . + a n 1 x n 1 F(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}
我们把它分成偶次 F 1 F_1 和奇次 F 2 F_2 两个部分
F 1 ( x ) = a 0 + a 2 x 2 + . . . + a n 2 x n 2 F_1(x)=a_0+a_2x^2+...+a_{n-2}x^{n-2}
F 2 ( x ) = a 1 x + a 3 x 3 + . . . + a n 1 x n 1 F_2(x)=a_1x+a_3x^3+...+a_{n-1}x^{n-1}
那么 F ( x ) = F 1 ( x 2 ) + x F 2 ( x 2 ) F(x)=F_1(x^2)+xF_2(x^2)
假设 k < n 2 k<\frac{n}{2}
当我们带入 x = ω n k x=\omega_n^k 的时候,用一下[性质2],式子是:
F ( ω n k ) = F 1 ( ω n 2 k ) + ω n k F 2 ( ω n 2 k ) F(\omega_n^k)=F_1(\omega_n^{2k})+\omega_n^kF_2(\omega_n^{2k})
利用[性质1],变形一下:
F ( ω n k ) = F 1 ( ω n 2 k ) + ω n k F 2 ( ω n 2 k ) F(\omega_n^k)=F_1(\omega_{\frac{n}{2}}^k)+\omega_n^kF_2(\omega_{\frac{n}{2}}^k)
此时我们再带入 x = ω n k + n 2 x=\omega_n^{k+\frac{n}{2}} ,得到:
F ( ω n k + n 2 ) = F 1 ( ω n 2 ( k + n 2 ) ) + ω n k + n 2 F 2 ( ω n 2 ( k + n 2 ) ) F(\omega_n^{k+\frac{n}{2}})=F_1(\omega_n^{2(k+\frac{n}{2})})+\omega_n^{k+\frac{n}{2}}F_2(\omega_n^{2({k+\frac{n}{2}})}) 利用[性质2],变形一下,得: F ( ω n k + n 2 ) = F 1 ( ω n 2 k ω n n ) + ω n k + n 2 F 2 ( ω n 2 k ω n n ) F(\omega_n^{k+\frac{n}{2}})=F_1(\omega_n^{2k}\cdot\omega_n^n)+\omega_n^{k+\frac{n}{2}}F_2(\omega_n^{2k}\cdot\omega_n^n) F ( ω n k + n 2 ) = F 1 ( ω n 2 k ) ω n k F 2 ( ω n 2 k ) F(\omega_n^{k+\frac{n}{2}})=F_1(\omega_n^{2k})-\omega_n^kF_2(\omega_n^{2k}) F ( ω n k + n 2 ) = F 1 ( ω n 2 k ) ω n k F 2 ( ω n 2 k ) F(\omega_n^{k+\frac{n}{2}})=F_1(\omega_{\frac{n}{2}}^k)-\omega_n^kF_2(\omega_{\frac{n}{2}}^k)
这两个式子有什么用呢?
实际上,我们在这里证明了每一个 F ( ω n k ) F(\omega_n^k) 都可以由某两个 F 1 / F 2 ( ω n 2 k ) F1/F2(\omega_{\frac{n}{2}}^k) 得到,所以如果知道了所有 F 1 ( ω n 2 k ) F 2 ( ω n 2 k ) F1(\omega_{\frac{n}{2}}^k)和F2(\omega_{\frac{n}{2}}^k) 的值,我们可以在O(n)时间内求出所有 F ( ω n k ) F(\omega_n^k) 的值。那么对于这个问题我们可以分而治之,也就是用分治递归解决。

离散傅里叶逆变换(IDFT)

我们如果要为这个过程再设计一种方法的话,那未免有点麻烦,我们可以尝试通过某张变换方式,是的IDFT也可以套用DFT的解决方法,这样就会方便很多。
首先考虑DFT的过程,可一转变为一个矩阵,如下:
[ ( ω n 0 ) 0 ( ω n 0 ) 1 ( ω n 0 ) n 1 ( ω n 1 ) 0 ( ω n 1 ) 1 ( ω n 1 ) n 1 ( ω n n 1 ) 0 ( ω n n 1 ) 1 ( ω n n 1 ) n 1 ] [ a 0 a 1 a n 1 ] = [ F ( ω n 0 ) F ( ω n 1 ) F ( ω n n 1 ) ] \begin{bmatrix} (\omega_n^0)^0 & (\omega_n^0)^1 & \cdots & (\omega_n^0)^{n-1} \\ (\omega_n^1)^0 & (\omega_n^1)^1 & \cdots & (\omega_n^1)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{n-1})^0 & (\omega_n^{n-1})^1 & \cdots & (\omega_n^{n-1})^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} F(\omega_n^0) \\ F(\omega_n^1) \\ \vdots \\ F(\omega_n^{n-1}) \end{bmatrix}
我们假设最左边的系数矩阵为A,那么我们构造一个矩阵B,使得 B i , j = ( ω n i ) j B_{i,j}=(\omega_n^{-i})^j ,那么B矩阵就是这个样子:
B = [ ( ω n 0 ) 0 ( ω n 0 ) 1 ( ω n 0 ) n 1 ( ω n 1 ) 0 ( ω n 1 ) 1 ( ω n 1 ) n 1 ( ω n ( n 1 ) ) 0 ( ω n ( n 1 ) ) 1 ( ω n ( n 1 ) ) n 1 ] \mathbf B = \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix} 我们让A与B相乘,假设结果为C,那么可以写出:
C i , j = k = 0 n 1 A i , k B k , j C_{i,j}=\sum_{k=0}^{n-1}A_{i,k}\cdot B_{k,j}
带入A和B的值,得到:
C i , j = k = 0 n 1 ω n i k ω n k j C_{i,j}=\sum_{k=0}^{n-1}\omega_n^{ik}\cdot\omega_n^{-kj}
C i , j = k = 0 n 1 ω n k ( i j ) C_{i,j}=\sum_{k=0}^{n-1}\omega_n^{k(i-j)}
由此我们可以得出:
i=j时: C i , j = n C_{i,j}=n
否则: C i , j = 1 ( ω n i j ) n 1 ω n i j = 0 C_{i,j}=\frac{1-(\omega_n^{i-j})^n}{1-\omega_n^{i-j}}=0
那么C矩阵实际上是单位矩阵的n倍!
那么我们就可以这么做:
[ a 0 a 1 a n 1 ] = 1 n [ ( ω n 0 ) 0 ( ω n 0 ) 1 ( ω n 0 ) n 1 ( ω n 1 ) 0 ( ω n 1 ) 1 ( ω n 1 ) n 1 ( ω n ( n 1 ) ) 0 ( ω n ( n 1 ) ) 1 ( ω n ( n 1 ) ) n 1 ] [ A ( ω n 0 ) A ( ω n 1 ) A ( ω n n 1 ) ] \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \frac{1}{n} \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix} \begin{bmatrix} A(\omega_n^0) \\ A(\omega_n^1) \\ \vdots \\ A(\omega_n^{n-1}) \end{bmatrix}
然而这就可以直接再做一遍FFT来解决。

代码实现

模板题:UOJ 34

#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
int n,m,bitn;
const double Pi=acos(-1.0);
struct comx{
	double x,y;
	comx (double x1=0,double y1=0){x=x1,y=y1;}
	comx operator +(comx b){return comx(x+b.x,y+b.y);}
	comx operator -(comx b){return comx(x-b.x,y-b.y);}
	comx operator *(comx b){return comx(x*b.x-y*b.y,x*b.y+y*b.x);}
}a[maxn],b[maxn];
void FFT(int lim,comx *a,int p){
	if (lim==1) return;
	comx a1[lim>>1],a2[lim>>1];
	for (int i=0;i<=lim;i+=2) a1[i>>1]=a[i],a2[i>>1]=a[i+1];
	FFT(lim>>1,a1,p); FFT(lim>>1,a2,p);
	comx omn=comx(cos(2.0*Pi/lim),1.0*p*sin(2.0*Pi/lim)),now=comx(1.0,0.0);
	for (int i=0;i<(lim>>1);i++){
		a[i]=a1[i]+now*a2[i];
		a[i+(lim>>1)]=a1[i]-now*a2[i];
		now=now*omn;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for (int i=0;i<=n;i++) scanf("%lf",&a[i].x);
	for (int i=0;i<=m;i++) scanf("%lf",&b[i].x);
	bitn=1; while (bitn<=(n+m)) bitn<<=1;
	FFT(bitn,a,1); FFT(bitn,b,1);
	for (int i=0;i<=bitn;i++) a[i]=a[i]*b[i];
	FFT(bitn,a,-1);
	for (int i=0;i<=n+m;i++) printf("%.0lf ",a[i].x/(1.0*bitn));
	printf("\n");
	return 0;
}

迭代版FFT(蝴蝶操作)

#include<bits/stdc++.h>
using namespace std;
const int maxn=(1<<20)+5;
const double Pi=acos(-1.0);
int n,m,limn,R[maxn];
struct C{
	double x,y;
	C(double xx=0,double yy=0){x=xx,y=yy;}
	C operator +(C b) {return C(x+b.x,y+b.y);}
	C operator -(C b) {return C(x-b.x,y-b.y);}
	C operator *(C b) {return C(x*b.x-y*b.y,x*b.y+b.x*y);}
}a[maxn],b[maxn],c[maxn],w[maxn];
void FFT(C a[],int lim){
	for (int i=0;i<lim;i++) if (R[i]>i) swap(a[R[i]],a[i]);
	for (int t=lim>>1,d=1;d<lim;d<<=1,t>>=1)
	for (int i=0;i<lim;i+=(d<<1))
	for (int j=0;j<d;j++){
		C p=w[t*j]*a[i+j+d];
		a[i+j+d]=a[i+j]-p,a[i+j]=a[i+j]+p;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for (int i=0;i<=n;i++) scanf("%lf",&a[i].x);
	for (int i=0;i<=m;i++) scanf("%lf",&b[i].x);
	int L; for (limn=1,L=0;limn<=n+m;limn<<=1,L++);
	for (int i=0;i<limn;i++)
		R[i]=(R[i>>1]>>1)|((i&1)<<(L-1)),w[i]=C(cos(2.0*Pi*i/limn),sin(2.0*Pi*i/limn));
	FFT(a,limn); FFT(b,limn);
	for (int i=0;i<limn;i++)
		c[i]=a[i]*b[i],w[i].y=-w[i].y;
	FFT(c,limn);
	for (int i=0;i<=n+m;i++) printf("%d ",(int)(c[i].x/limn+0.5));
	printf("\n");
	return 0;
}

一些练习:

hdu1402

#include<bits/stdc++.h>
using namespace std;
const int maxn=(1<<17)+5;
const double Pi=acos(-1.0);
int n,m,limn,R[maxn],ans[maxn];
char s[maxn];
struct comx{
	double x,y;
	comx(double xx=0,double yy=0){x=xx,y=yy;}
	comx operator +(const comx b){return comx(x+b.x,y+b.y);}
	comx operator -(const comx b){return comx(x-b.x,y-b.y);}
	comx operator *(const comx b){return comx(x*b.x-y*b.y,x*b.y+b.x*y);}
}a[maxn],b[maxn],w[maxn];
void exc(comx *a,int len){for (int i=0;i<=len;i++) a[len-i].x=s[i]-'0';}
void fft(comx *a,int lim){
	for (int i=0;i<lim;i++) if (R[i]>i) swap(a[R[i]],a[i]);
	for (int t=lim>>1,d=1;d<lim;d<<=1,t>>=1)
	for (int i=0;i<lim;i+=(d<<1))
	for (int j=0;j<d;j++){
		comx p=w[t*j]*a[i+j+d];
		a[i+j+d]=a[i+j]-p,a[i+j]=a[i+j]+p;
	}
}
int main(){
	while (~scanf("%s",s)){
		for (int i=0;i<=(1<<17);i++) a[i].x=a[i].y=0.0,b[i].x=b[i].y=0.0,R[i]=0,w[i].x=w[i].y=0.0;
		n=strlen(s); exc(a,--n);
		scanf("%s",s);
		m=strlen(s); exc(b,--m);
		int l=0; limn=1;
		while (limn<=n+m) limn<<=1,l++;
		for (int i=0;i<limn;i++){
			R[i]=((R[i>>1]>>1)|((i&1)<<(l-1)));
			w[i]=comx(cos(2.0*Pi/limn*i),sin(2.0*Pi/limn*i));
		}
		fft(a,limn); fft(b,limn);
		for (int i=0;i<limn;i++) a[i]=a[i]*b[i],w[i].y=-w[i].y;
		fft(a,limn);
		memset(ans,0,sizeof(ans));
		for (int i=0;i<limn;i++) ans[i]=(int)(a[i].x/limn+0.5);
		for (int i=0;i<=n+m+1;i++) ans[i+1]+=ans[i]/10,ans[i]%=10;
		int maxx=0;
		for (int i=0;i<=n+m+1;i++) if (ans[i]>0) maxx=i;
		for (int i=maxx;i>=0;i--) printf("%d",ans[i]); printf("\n");
	}
	return 0;
}

Bzoj 4503

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=(1<<18)+5;
const double Pi=acos(-1.0);
int n,m,limn,R[maxn];
char S[maxn],T[maxn];
struct comx{
	double x,y;
	comx(double xx=0,double yy=0){x=xx,y=yy;}
	comx operator +(const comx b){return comx(x+b.x,y+b.y);}
	comx operator -(const comx b){return comx(x-b.x,y-b.y);}
	comx operator *(const comx b){return comx(x*b.x-y*b.y,x*b.y+y*b.x);} 
}a[maxn],b[maxn],c[maxn],d[maxn],w[maxn];
double s;
void pre(){
	int L=0; limn=1; while (limn<=n+m) limn<<=1,L++;
	for (int i=0;i<limn;i++){
		R[i]=((R[i>>1]>>1)|((i&1)<<(L-1)));
		w[i]=comx(cos(2*Pi/limn*i),sin(2*Pi/limn*i));
	}
}
void FFT(comx *a,int lim){
	for (int i=0;i<lim;i++) if (R[i]>i) swap(a[R[i]],a[i]);
	for (int t=lim>>1,d=1;d<lim;d<<=1,t>>=1)
	for (int i=0;i<lim;i+=(d<<1))
	for (int j=0;j<d;j++){
		comx p=w[t*j]*a[i+j+d];
		a[i+j+d]=a[i+j]-p,a[i+j]=a[i+j]+p;
	}
}
void doit(comx *p,comx *q){
	FFT(p,limn); FFT(q,limn);
	for (int i=0;i<limn;i++) p[i]=p[i]*q[i],w[i].y=-w[i].y;
	FFT(p,limn);
	for (int i=0;i<limn;i++) w[i].y=-w[i].y,p[i].x/=limn;
}
ll cal(double x){return (ll)(x+0.5);}
int main(){
	scanf("%s",S); scanf("%s",T);
	n=strlen(S)-1; m=strlen(T)-1;
	for (int i=0;i<=n;i++) c[i].x=S[i]-'a'+1;
	for (int i=0;i<=m;i++)
		if (T[i]!='?') b[m-i].x=T[i]-'a'+1; else b[m-i].x=0;
	for (int i=0;i<=n;i++) a[i].x=c[i].x*c[i].x;
	for (int i=0;i<=m;i++) d[i].x=b[i].x*b[i].x,s+=b[i].x*b[i].x*b[i].x;
	pre(); doit(a,b); doit(c,d);
	int ans=0;
	for (int i=m;i<=n;i++)
		if (cal(a[i].x)-2*cal(c[i].x)+cal(s)==0) ans++;
	printf("%d\n",ans);
	for (int i=m;i<=n;i++)
		if (cal(a[i].x)-2*cal(c[i].x)+cal(s)==0) printf("%d\n",i-m);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/DYT_B/article/details/84672286