很詳細的FFT(快速傅里葉變換)概念與實現


FFT

首先要說明一個誤區,很多人認為FFT只是用來處理多項式乘的,其實FFT是用來實現多項式的系數表示法和點值表示法的快速轉換的,所以FFT的用處遠不止多項式乘。

FFT的前置知識:點值表示法,復數運算,三角函數。

多項式的系數表示法和點值表示法

系數表示法

\[A(x)=\sum_{i=0}^n a_i*x^i \]

點值表示法

不妨將A視為關於x的函數,點值表示法就是在A的圖像上取n個點,則該多項式可以被這n個點唯一確定。

\[a_i=A(x_i)\\ A(x)=\{(x_1,a_1),(x_2,a_2),\dots,(x_n,a_n)\} \]

點值表示法有什么好處呢?

我們知道系數表示法下兩多項式相乘是\(O(n^2)\),但在點值表示法下奇跡出現了:

\[A(x)=\{(x_1,a_1),(x_2,a_2),\dots,(x_n,a_n)\} \\ B(x)=\{(x_1,b_1),(x_2,b_2),\dots,(x_n,b_n)\} \\ A(x)*B(x)=\{(x_1,a_1*b_1),(x_2,a_2*b_2),\dots,(x_n,a_n*b_n)\} \]

顯然這個是可以O(n)實現的。雖然但是,我們幾乎不會在計算中用到點值表示法,但這也給了我們一個解決多項式乘的思路。系數轉點值,相乘,點值轉系數。

又很顯然,我們可以隨便取n個數往函數里帶,可惜這樣又使復雜度回到了\(O(n^2)\)

於是FFT出現了,FFT使我們可以用\(O(n\log n)\)的復雜度將系數轉換成一組特殊的點值,並再把點值轉回系數。

復數的計算

簡單的理解,復數就是實數加虛數,多少都知道點虛數吧,沒錯,知道點就夠了。

\[i=\sqrt{-1}\\ z_1=a+bi\\ z_2=c+di\\ z_1+z_2=a+c+(b+d)i\\ z_1-z_2=a-c+(b-d)i\\ z_1*z_2=ac-bd+(da+bc)i \]

單位根

百度一下

記住以下性質(感興趣可以自己推推,就是基礎的三角函數)

\[\omega_n^k=cosk\frac{2\pi}{n}+sink\frac{2\pi}{n} \\ \omega_n^0=\omega_n^n=1\\ \omega_{2n}^{2k}=\omega_n^k \]

好了,現在你已經掌握了所有FFT的前置知識了,自己來推推FFT吧

正式開始FFT

\(\omega_n^0,\dots,\omega_n^{n-1}\)這n個數帶入得到點值表示,於是:

\[A(x)=a_0+a_1*x+a_2*{x^2}+a_3*{x^3}+a_4*{x^4}+a_5*{x^5}+ \dots+a_{n-2}*x^{n-2}+a_{n-1}*x^{n-1}\\ A(x)=(a_0+a_2*{x^2}+a_4*{x^4}+\dots+a_{n-2}*x^{n-2})+(a_1*x+a_3*{x^3}+a_5*{x^5}+ \dots+a_{n-1}*x^{n-1})\\ A_1(x)=a_0+a_2*{x}+a_4*{x^2}+\dots+a_{n-2}*x^{\frac{n}{2}-1}\\ A_2(x)=a_1+a_3*{x}+a_5*{x^2}+ \dots+a_{n-1}*x^{\frac{n}{2}-1}\\ A(x)=A_1(x^2)+xA_2(x^2)\\ \]

我們將\(ωkn(k<n2)ωnk(k<n2)\)代入得

\[A(\omega_n^k)=A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k})\\ A(\omega_n^{k+\frac{n}{2}})=A_1(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}(\omega_n^{2k+n})=A_1(\omega_n^{2k}*\omega_n^n)-\omega_n^kA_2(\omega_n^{2k}*\omega_n^n)=A_1(\omega_n^{2k})-\omega_n^kA_2(\omega_n^{2k}) \]

摘自attak的blog

觀察這兩個式子

\[A(\omega_n^k)=A_1(\omega_n^{2k})+\omega_n^kA_2(\omega_n^{2k})\\\ A(\omega_n^{k+\frac{n}{2}})=A_1(\omega_n^{2k})-\omega_n^kA_2(\omega_n^{2k})\\ \]

顯然(怎么又是顯然)只要求出\(A_1(\omega_n^{2k})\)\(\omega_n^kA_2(\omega_n^{2k})\)就可以得出\(A(\omega_n^k)\)\(A(\omega_n^{k+\frac{n}{2}})\),而\(A_1(\omega_n^{2k})\)\(\omega_n^kA_2(\omega_n^{2k})\)又可以進一步遞歸,正是這一點性質使FFT的復雜度達到了優秀的\(O(n\log n)\)

IFFT

前面提到了,系數轉點值,相乘,點值轉系數,所以我們還需要能在\(O(n\log n)\)復雜度下完成點值轉系數,這就是IFFT,快速傅利葉逆變換。

可以將傅里葉變換和傅立葉逆變換的公式表示寫出來(我也不會推逆變換)。

A(x) 表示多項式的系數表示法 B(x)表示多項式的點值表示法

\[FFT\ B(x)=\sum_{i=0}^{N-1} A(k)\omega_N^{ik}\\ IFFT\ A(x)=\frac 1 N \sum _{i=0} ^{N-1} B(k)\omega_n^{-ik}\\ \]

IFFT就是在把FFT的\(\omega_n^{ik}改成\omega_n^{-ik}\),然后再乘個\(\frac 1 N\)

考慮\(\omega_n^k\)的幾何意義(高一三角函數)可以得到

\[\omega_n^{ik}=a+bi\\ \omega_n^{-ik}=a-bi \]

所以IFFT只需要在FFT上做一點改動。

千萬別忘了最后乘個\(\frac 1 N\)

實現

個人認為這是所有算法最難也最重要的部分,然而很多blog都是將這部分一筆帶過,所以我決定來詳細的講講。

遞歸寫法

竟然是遞歸,那必然有遞歸極限。每次遞歸多項式的項數剩下一半,只剩一項時\(A(x)=a_1\)\(x\)無關,所以直接返回就好了。

在求出了\(A_1(\omega_n^{2k})\)\(\omega_n^kA_2(\omega_n^{2k})\)后做兩次多項式加減可以得出\(A(\omega_n^k)\)\(A(\omega_n^{k+\frac{n}{2}})\)

由於多次的遞歸和數組新建和賦值使遞歸寫法的常數大的出奇,所以我們需要更好的寫法。

重難點來了!

遞推做法

首先觀察這張圖。

1101696-20180212074250859-1560811086.png (520×492) (cnblogs.com)

我們的遞歸做法就是從上向下將原數列對半拆,再合並。

我們的合並順序就是從下到上合並,詳細的說先合並\(a_0和a_4,a_2和a_6,a1和a_5,a_3和a_7,然后a_0,a_4合並好的整體與a_2和a_6合並好的整體再合並\dots\)

觀察最上和最下層的數列的二進制表示,發現就是將二進制翻轉了。我們有這個神仙操作可以快速的翻轉二進制。

rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1)); ///rev[i]保存i二進制翻轉后的數

於是我們可以\(O(n)\)得到最下層的數列,再依次往上推,即不用遞歸也不用開大量數組,讓代碼變得飛快!

void fft(cp *a,int n,int inv) 
{
    int bit=0;
    while((1<<bit)<n)bit++;
    for(int i=0;i<=n-1;i++)
    {
        rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));	///翻轉二進制 3(2)=011 to 6(2)=011
        if(i<rev[i]) swap(a[i],a[rev[i]]); 
    }
    for(int mid=1;mid<n;mid*=2)	///mid表示當前合並的行中每一塊的長度的一半 
    {
        cp ur(cos(pi/mid),inv*sin(pi/mid));	///ur代表單位根 
        for(int i=0;i<n;i+=mid*2) ///i表示當前枚舉到這一橫行的哪一塊的開頭下標 
        {
            cp tmp(1,0);
            for(int j=0;j<mid;j++,tmp*=ur) ///與遞歸的寫法一樣,合並這一塊和后面一塊。 
            {
                cp x=a[i+j],y=tmp*a[i+j+mid];
                a[i+j]=x+y,a[i+j+mid]=x-y;
            }
	}
    }
}

luogu FFT板子

#include<bits/stdc++.h>
using namespace std;
typedef complex<double> cp;
const int N=(1<<21)+10;
const double pi=acos(-1);
cp a[N],b[N];
int n,m,bit,lim,rev[N];
void fft(cp *a,int type)
{
	for(int i=0;i<lim;i++)
		if(i<rev[i]) swap(a[i],a[rev[i]]);
	for(int mid=1;mid<lim;mid*=2)
	{
		cp ur(cos(pi/mid),type*sin(pi/mid));
		for(int i=0;i<lim;i+=mid*2)
		{
			cp tmp(1,0);
			for(int j=0;j<mid;j++,tmp*=ur)
			{
				cp x=a[i+j],y=tmp*a[i+j+mid];
				a[i+j]=x+y,a[i+j+mid]=x-y;	
			} 
		}
	}
}
int main()
{
	cin>>n>>m;
	for(int i=0;i<=n;i++)
		cin>>a[i];
	for(int i=0;i<=m;i++)
		cin>>b[i];
	for(lim=1;lim<=n+m;lim<<=1)
		bit++;
	for(int i=0;i<lim;i++)
		rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
	fft(a,1),fft(b,1);
	for(int i=0;i<=lim;i++)
		a[i]*=b[i];
	fft(a,-1);
	for(int i=0;i<=n+m;i++)
		cout<<(int)(0.5+a[i].real()/lim)<<' ';
	return 0;
}

繼續學習NTT


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM