快速傅立葉變換 學習筆記

Cyan_wind發表於2024-08-19

快速傅立葉變換 學習筆記

簡介:

卷積是形如 \(C_k=\sum_{i\oplus j==k} A_i*B_j\) 的式子,其中 \(\oplus\) 為表示某種運算。

而快速傅立葉變換(FFT)用於在 \(O(n\log n)\) 的時間複雜度內求加法卷積。

對於一個 \(k\) 次多項式,如果我們知道了它在 \(k+1\) 個點上的值,那麼我們可以求出這個多項式。而兩個多項式在同一個點上的值相乘的話,就可以得到 這兩個多項式相乘後的多項式 在這個點上的值。FFT 的主要思路就是先將兩個多項式在若干個點上的取值,然後相乘,再透過點值求出兩個多項式相乘的結果。

單位根

先考慮求點值。我們肯定不能暴力選擇點去代入求值,這樣的時間複雜度是 \(O(n^2)\) 的,要找一些有特殊性質的點去代入。

於是我們在複數域找到了單位根,它是在複平面上的單位圓上的點,表示為 \(\omega_n^k\) ,它的意義是將單位圓分割成 \(n\) 份,按順指標方向的第 \(k\) 個點所表示的值。兩個複數相乘時,它們的輻角相加(從實軸的正半軸開始,按逆時針旋轉),模長相乘(該點到原點的距離)。於是我們可以得到單位根的以下性質:

\[\omega_n^i*\omega_n^j=\omega_n^{i+j}\\ \omega_n^k=\omega_n^{k\%n}\\ \omega_{n*i}^{k*i}=\omega_n^k\\ \omega_n^{k+n/2}=-\omega_n^k(當 n 為偶數時) \]

對於最後一條,我們可以將 \(n/2\) 看做旋轉半個圓周,所以就是取相反數。

對於一個 \(n\) 次多項式,我們看看將單位根代入會有什麼性質:

先將多項式 \(F[x]\) 拆成兩個部分: \(FL[x]=x^0+x^2+x^4...,FR[x]=x^1+x^3+x^5...\) 。然後就可以將多項式 \(F[x]\) 這樣表示:

\[F[x]=FL[x^2]+x*FR[x^2] \]

再分別將 \(\omega_n^k\)\(\omega_n^{k+n/2}\) 代入:

\[F[\omega_n^k]=FL[(\omega_n^k)^2]+\omega_n^k*FR[(\omega_n^k)^2]\\ =FL[\omega_{n/2}^k]+\omega_n^k*FR[\omega_{n/2}^k]\\ F[\omega_n^{k+n/2}]=FL[(\omega_n^{k+n/2})^2]+\omega_n^{k+n/2}*FR[(\omega_n^{k+n/2})^2]\\ =FL[\omega_{n/2}^{k+n/2}]-\omega_n^k*FR[\omega_{n/2}^{k+n/2}]\\ =FL[\omega_{n/2}^k]-\omega_n^k*FR[\omega_{n/2}^k] \]

發現上下兩式只有後面一項的符號不同,所以如果我們求出了 \(FL[x]\)\(FR[x]\)\(\omega_{n/2}^{0..n/2-1}\) 上的值,我們就可以求出 \(F[x]\)\(\omega_n^{0...n-1}\) 上的值了,這樣做一次的時間複雜度是 \(O(n)\) 的。同時我們可以發現,對於 \(FL[x]\)\(FR[x]\) ,其實可以看做一個相同的子任務,再次遞迴下去求解。一共需要遞迴 \(log\ n\) 次。

但是此時我們又有一個問題:如果某個 \(n\) 不能被整除了怎麼辦?我們可以給它在後面補位,係數設為 0 就行。再觀察一下我們遞迴到最下面一層時,係數的編號與第一層相比有什麼特點。可以發現它是二進位制位的反轉,所以我們可以用數位DP的方法求出反轉後的編號,這樣就不用一遍一遍遞迴下去求了。

tr[i]=(tr[i>>1]>>1)|(i&1)?limit>>1:0

求多項式

透過上面的步驟,我們已經將 \(n\) 個點的值求出來了,接下來就要透過這 \(n\) 個點的值求多項式。

設點值構成的序列為 \(G[x]\) 。然後我們可以列出以下式子:

\[G[x]=\sum_{i=0}^{n-1}\omega_n^i*F[i] \]

然後可以得到以下結論:

\[F[x]*n=\sum_{i=0}^{n-1}\omega_n^{-i}*G[i] \]

這個式子的證明就是將上面的式子代入。此時我們發現,這個式子和我們求點值的式子十分相似,只是在單位根上有點區別。我們可以發現,在單位圓上, \(\omega_n^{-1}\) 所在的點和 \(\omega_n^1\) 所在的點,它們關於實軸對稱。於是我們也可以用同樣的方法來求上面的式子。最後記得除以 \(n\) 就是我們最終的多項式。

Code

#include<cstdio>
#include<cmath>
using namespace std;
const int N=3e6+5;const double PI=6.28318530717958646;
struct Date{
	double x,y;
	
	Date operator*(Date v) {
		Date res;res.x=x*v.x-y*v.y;res.y=x*v.y+y*v.x;return res;
	};
	Date operator+(Date v) {
		Date res;res.x=x+v.x;res.y=y+v.y;return res;
	};
	Date operator-(Date v) {
		Date res;res.x=x-v.x;res.y=y-v.y;return res;
	};
};Date f[N],g[N],w[N];
int tr[N],n,m;
void swap(double &x,double &y)
{
	double t=x;x=y;y=t;
}
void FFT(int flag)
{
	int i,j,len;Date t;
	w[0].x=1;w[0].y=0;
	w[1].x=cos(PI/n);w[1].y=sin(PI/n)*flag;
	for(i=1;i<n;i++)
	w[i]=w[i-1]*w[1];
	for(i=0;i<n;i++)
	{
		if(i<tr[i])
		{
			swap(f[i].x,f[tr[i]].x);swap(f[i].y,f[tr[i]].y);
		}
	}
	for(len=1;len<n;len=len<<1)
	{
		for(i=0;i<n;i+=2*len)
		{
			for(j=0;j<len;j++)
			{
				t=f[i+j+len]*w[n/len/2*j];
				f[i+j+len]=f[i+j]-t;f[i+j]=f[i+j]+t;
			}
		}
	}
}
int main()
{
	int i,h,len;
	scanf("%d%d",&n,&m);
	for(i=0;i<=n;i++)
	scanf("%lf",&f[i].x);
	for(i=0;i<=m;i++)
	scanf("%lf",&g[i].x);
	h=0;len=n+m;
	while((1<<h)<n+m+1)
	h++;
	n=1<<h;
	for(i=0;i<n;i++)
	tr[i]=(tr[i>>1]>>1)|((i&1)?n>>1:0);
	FFT(1);
	for(i=0;i<n;i++)
	{
		swap(f[i].x,g[i].x);swap(f[i].y,g[i].y);
	}
	FFT(1);
	for(i=0;i<n;i++)
	f[i]=f[i]*g[i];
	FFT(-1);
	for(i=0;i<=len;i++)
	printf("%d ",int(f[i].x/n+0.49));
	return 0;
}

相關文章