[模板] 快速傅里葉變換(FFT)

多項式

假設有nn次多項式:
i=0naixi\sum_{i=0}^na_ix^i
如何表示這個多項式.
首先想到的是用所有係數表示,即:
{an,an1,an2,...,a2,a1,a0}\{a_n,a_{n-1},a_{n-2},...,a_2,a_1,a_0\}
我們將這樣的表示方法爲係數表示法.
但是,係數表示法在表示多項式相加和相乘很不方便.
譬如又有nn次表達式:
i=0nbixi\sum_{i=0}^nb_ix^i
那麼將兩式子相乘的時間複雜度是O(n2)O(n^2)的.
如果a|a|=10510^5,這種算法就不是那麼優越.
所幸,我們還有另一種表示方法——點值表示法.
設想,如果我們用足夠多的值對(x,y) 表示這個函數:
y=i=0naixiy=\sum_{i=0}^na_ix^i
表達式的係數是可以確定的.
美妙的是,在點值表達式下,多項式加法和乘法是可以在O(n)O(n)實現的!
比如,有點對
(a1,X1)(a2,X2)...(an,Xn)(a_1,X_1)(a_2,X_2)...(a_n,X_n)
(a1,Y1)(a2,Y2)...(an,Yn)(a_1,Y_1)(a_2,Y_2)...(a_n,Y_n)
那麼相乘得
(a1,X1Y1)(a2,X2Y2)...(an,XnYn)(a_1,X_1Y_1)(a_2,X_2Y_2)...(a_n,X_nY_n)
妙啊

轉換

但是,我們在實際生活中並不經常使用點值表示法,而是經常使用係數表示法.
於是,便有了兩者之間的轉換.
係數表示法和點值表示法可以用如下矩陣乘法轉換:

[1  x1  x12 ... x1n1  x2  x22 ... x2n.   .   .   .   .1  xn  xn2 ... xnn][a0a1...an]=[X1X2...Xn]\left[\begin{array}{cc} 1\ \ x_1\ \ x_1^2\ ...\ x_1^n\\ \\ 1\ \ x_2\ \ x_2^2\ ...\ x_2^n\\ \\ .\ \ \ .\ \ \ .\ \ \ .\ \ \ .\\ \\ 1\ \ x_n\ \ x_n^2\ ...\ x_n^n\\ \end{array}\right] \left[\begin{array}{cc} a_0\\ \\ a_1\\ \\ ...\\ \\ a_n \end{array}\right]= \left[\begin{array}{cc} X_1\\ \\ X_2\\ \\ ...\\ \\ X_n \\ \end{array}\right]
這種變換,叫作離散傅里葉變換(DFT).
反之,由點值表示法退回係數表示法,叫作離散傅里葉逆變換(IDFT).
花了好久點個讚唄
但是發現,這樣轉換時間複雜度還是O(n2)O(n^2)的!
所以,就有了快速傅里葉變換(FFT).

快速傅里葉變換

又稱(Fast Fourier Transform)(FFT),詳情見Mr.Du.

快速傅里葉變換是離散傅里葉變換的快速版本.這還用說嗎
它可以以優秀的O(nlogn)O(nlogn)解決這個問題.
舉個例子:
我們代入a=1a=1這個特殊值,那麼可以得到X=i=0naiX=\sum_{i=0}^na_i.
我們代入a=1a=-1a=0a=0也可以得到特殊值.
但是,這樣的特殊值總是有限的.
所以,我們將眼光轉向別處——複數域.
比如說有一個複數座標系:
在這裏插入圖片描述
這個圓叫作單位圓沒錯我們見過上面的點都可以乘方到1.
如果想要乘方nn次,那麼這些值就是這樣分佈的:
在這裏插入圖片描述
這是n=8n=8的情況.最好畫了不是嗎
//下面所有nn都是22的冪,注意!
我們稱這樣的數列爲{ω1,ω2,...,ωn}\{\omega_1,\omega_2,...,\omega_n\}.(當然是乘方n次後爲1的 )
並稱這些爲 11nn次單位根 .

鋪墊

由神奇而美妙的歐拉公式:

eix=cosx+isinxe^{ix}=\cos x+i\sin x

還有複數的極角表達式:

(a,θ1)(b,θ2)=(ab,θ1+θ2)(a, \theta_1)(b,\theta_2)=(ab,\theta_1+\theta_2)

以及複數運算滿足交換律和結合律.
我們就可以以這些爲鋪墊,進行接下來的研究了 ?

定理

下面的定理不再證明,可以結合奆老博客和Mr.Du證明:
//提示:歐拉定理和複平面(上圖)是個好東西.

ωn1=cos(2π/n)+isin(2π/n)\omega_n^1=\cos(2\pi/n)+i\sin(2\pi/n)
對於ωn\omega_n序列,ωk=ω1k\omega_k=\omega_1^k(很實用)
ω2n2k=ωnk\omega_{2n}^{2k}=\omega_n^k
ωnk+n2=ωnk\omega_n^{k+\frac{n}{2}}=-\omega_n^k
ωnk+n=ωnk\omega_n^{k+n}=\omega_n^k

算法構建

經過這麼一波操作之後,好像什麼都沒做
因爲直到現在,我們還是沒有在這個數據上構造優化算法.
構造那麼神奇的數據,不實現又有何用
要不我們先將一個ωn\omega_n代入試試.
比如我們設定
A(n)=i=0nanωniA(n)=\sum_{i=0}^na_n\omega_n^i
看上去沒什麼變化…
但是我把它的奇數指數和偶數指數分開,分別寫成:
A1(n)=a0+a2ωn+...+anωnn2A_1(n)=a_0+a_2\omega_n+...+a_n\omega_n^\frac{n}{2}

A2(n)=a1ωn+a3ωn2+...+an1ωnn2A_2(n)=a_1\omega_n+a_3\omega_n^2+...+a_{n-1}\omega_n^\frac{n}{2}
//nn22的冪.
所以我們震驚的發現,竟然有這樣的表達式!
A(x)=A1(x2)+ωnA2(x2)A(x)=A_1(x^2)+\omega_nA_2(x^2)
哇我真棒
但是,這還是沒有用…
在仔細研究爲什麼需要使用複數.
因爲,ωnk+n2=ωnk\omega_n^{k+\frac{n}{2}}=-\omega_n^k!
所以,我們取x=ωnk+n2x=\omega_n^{k+\frac{n}{2}}得:
A(x)=A1(x2)ωnA2(x2)A(x)=A_1(x^2)-\omega_nA_2(x^2)
整理一下得(k<n2k<\frac{n}{2}):
A(ωnk)=A1(ωn2k)+ωnkA2(ωn2k)A(\omega_n^k)=A_1(\omega_\frac{n}{2}^k)+\omega_n^kA_2(\omega_\frac{n}{2}^k)
A(ωnk+n2)=A1(ωn2k)ωnkA2(ωn2k)A(\omega_n^{k+\frac{n}{2}})=A_1(\omega_\frac{n}{2}^k)-\omega_n^kA_2(\omega_\frac{n}{2}^k)
出現了!FFT!

IFFT

看似好像不那麼容易.
但是我們已經會FFT了!
在分支向下的時候,保存每一項的係數,
具體怎麼保存呢?
在轉換的時候,我們乘的ωn\omega_n.
轉換回來的時候,就乘上它的共軛複數 What?你不會?
於是,我們就很快活

遞歸版FFT&IFFT

題目傳送門
//爲什麼叫BF很快揭曉
隨便打個真·暴力上去…

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
long long a[N],b[N],c[N];
int n,m;
int main()
{
	//True BF
	//O(mn)...
    scanf("%d%d",&m,&n);
    for (int i=0;i<=m;i++) scanf("%d",&a[i]);
    for (int i=0;i<=n;i++) scanf("%d",&b[i]);
    for (int i=0;i<=m;i++)
        for (int j=0;j<=n;j++)
            c[i+j]+=a[i]*b[j];
    for (int i=0;i<=n+m;i++)
        printf("%d ",c[i]);
    return 0;
}

Result:
在這裏插入圖片描述
//哇竟然能有55分!
而上面的諸多公式告訴我們,FFT可以完美解決這好像就是一道FFT模板題…
根據我們的FFT算法,很容易想到遞歸實現FFT:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const double pi=acos(-1.0); 
const int N=5e6+5;
struct complex
{
	double x,y;
	complex(double _x=0,double _y=0) {x=_x,y=_y;}
}a[N],b[N];
complex operator + (complex a,complex b) {return complex(a.x+b.x,a.y+b.y);}
complex operator - (complex a,complex b) {return complex(a.x-b.x,a.y-b.y);}
complex operator * (complex a,complex b) {return complex(a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x);}
void FFT(int n,complex *a,int inv)
{
	if (n==1) return;
	int k=n>>1;
	complex a1[k],a2[k];
	for (int i=0;i<n;i+=2) a1[i>>1]=a[i],a2[i>>1]=a[i+1];//A1和A2
	FFT(k,a1,inv);//
	FFT(k,a2,inv);//分治
	complex Wn=complex(cos(2.*pi/n),inv*sin(2.*pi/n)),w=complex(1,0);//omegaN和單位元
	for (int i=0;i<k;i++,w=w*Wn)
	{
		a[i]=a1[i]+w*a2[i];
		a[i+k]=a1[i]-w*a2[i];//一次推兩個
	}
}
int main()
{
	int n,m;
	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 p=1;
	while (p<=n+m) p<<=1;
	FFT(p,a,1),FFT(p,b,1);//係數轉點值
	for (int i=0;i<=p;i++) a[i]=a[i]*b[i];
	FFT(p,a,-1);//點值轉系數
	for (int i=0;i<=n+m;i++) printf("%d ",(int)(abs)(a[i].x/p+0.5));
	return 0;
}

然而我竟然AC了!這是怎麼回事?
Result:

在這裏插入圖片描述
這還怎麼提出迭代版FFT!別提了

迭代版FFT&IFFT

接下來,我們將它優化一下:
這裏是經過FFT分治後的序列

初始序列(id) 0 1 2 3 4 5 6 7
1次操作 0 2 4 6 1 3 5 7
2次操作 0 4 2 6 1 5 3 7
2進制編碼 000 100 010 110 001 101 011 111
2進制逆序編碼 000 001 010 011 100 101 110 111

我真聰明
可以發現,最後的編碼是按逆序升序排列的.
換句話說:每個下標位置處理後的位置是它二進制逆序後的位置!

用這個短小精悍的代碼就可以解決一切問題:

//名叫Rader算法
int p=1,L=0;
while (p<=n+m) p<<=1,L++;
for (int i=0;i<p;i++)
	order[i]=(order[i>>1]>>1)|((i&1)<<(L-1));
	//(i&1)<<(L-1) 是爲了考慮後面的1<<(L-1)個數

真有趣 ?
接下來,我們就更快活開心了 ?

蝴蝶效應

屬於小優化的範疇.
主要是因爲複數乘法過於緩慢,所以可以預先處理好.
而已qwq.不要想到龍捲風那裏去了

Code

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=5e6+5;
const double pi=acos(-1.);
struct complex 
{
	double x,y;
	complex(double _x=0,double _y=0) {x=_x,y=_y;}
}a[N],b[N];
complex operator + (complex a,complex b) {return complex(a.x+b.x,a.y+b.y);}
complex operator - (complex a,complex b) {return complex(a.x-b.x,a.y-b.y);}
complex operator * (complex a,complex b) {return complex(a.x*b.x-a.y*b.y,a.x*b.y+b.x*a.y);}
int order[N];
int p=1,L;
void FFT(complex *a,int inv)
{
	for (int i=0;i<p;i++) 
		if (i<order[i]) swap(a[i],a[order[i]]);
	for (int l=1;l<p;l<<=1)
	{
		complex Wn(cos(pi/l),inv*sin(pi/l));
		for (int R=l<<1,j=0;j<p;j+=R)
		{
			complex w(1,0);
			for (int k=0;k<l;k++,w=w*Wn)
			{
				complex x=a[j+k],y=w*a[j+l+k];//B
				a[j+k]=x+y;
				a[j+l+k]=x-y;
			}
		}
	}	
} 
int main()
{
	int n,m;
	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);
	while (p<=n+m) p<<=1,L++;
	for (int i=0;i<p;i++)
		order[i]=(order[i>>1]>>1)|((i&1)<<(L-1));
	FFT(a,1),FFT(b,1);
	for (int i=0;i<=p;i++) a[i]=a[i]*b[i];
	FFT(a,-1);
	for (int i=0;i<=n+m;i++)
		printf("%d ",(int)(a[i].x/p+0.5));
	return 0;
}

Result:
在這裏插入圖片描述
感謝奆老關注 qwq ?

後記

FFT,又稱Fast-Fast-TLE…
這裏爲什麼不直接從&lt;complex&gt;&lt;complex&gt;這個庫裏調用complexcomplex?
因爲…太慢了…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章