前言 關(guān)于為什么又開始在這個(gè)博客上寫算法
FFT是我高中學(xué)競賽階段接觸到的最后一個(gè)算法,也是我高中一直沒有學(xué)會的算法之一,在高中畢業(yè)的時(shí)候認(rèn)為自己大學(xué)本科一定學(xué)的是CS,所以想要把這些自己之前沒有學(xué)會的算法都再琢磨一下,可是基于當(dāng)時(shí)的知識儲備我還完全不知道復(fù)數(shù)域和復(fù)平面以及多項(xiàng)式的各種定理,于是半途而廢了,F(xiàn)FT也成了在算法學(xué)習(xí)上我的一個(gè)心結(jié)之一。
但是巧合的是,我大學(xué)本科并沒有學(xué)CS,而是學(xué)了數(shù)學(xué),這些我一直未曾學(xué)會的算法也就被一直擱置了兩年,直到昨天我在上數(shù)據(jù)結(jié)構(gòu)課時(shí),數(shù)學(xué)院數(shù)據(jù)結(jié)構(gòu)課所學(xué)內(nèi)容跟OI比起來簡直不值一提,于是無聊的我突發(fā)奇想翻開了自己之前洛谷的提交記錄,發(fā)現(xiàn)FFT的版子還一直放在我的題單,于是就想利用這些時(shí)間把這些之前沒學(xué)懂的算法解決掉好了,也為明年考研跨考CS打下基礎(chǔ)。
這次在數(shù)學(xué)院歷練了兩年多的我拿著高等代數(shù)數(shù)學(xué)分析復(fù)變函數(shù)的知識再來看FFT,頓時(shí)覺得這些知識難度與我兩年前看的時(shí)候相比要簡單了許多,在昨天晚上解決了自己所有的疑難點(diǎn)實(shí)現(xiàn)了代碼拿到了AC。
看到我的這個(gè)博客之前發(fā)布的線段樹網(wǎng)絡(luò)流等等算法講解總瀏覽量都接近一萬了,那就繼續(xù)吧,讓這個(gè)斷更了4年的博客重新活起來!
奈芙蓮封面是這個(gè)博客的靈魂!!
問題背景
多項(xiàng)式乘法
給定一個(gè)次多項(xiàng)式
和
次多項(xiàng)式
,求出
與
的卷積。
輸入格式
第一行兩個(gè)整數(shù)。
接下來一行個(gè)數(shù)字,從低到高表示
的系數(shù)。
接下來一行個(gè)數(shù)字,從低到高表示
的系數(shù)。
輸出格式
一行個(gè)數(shù)字,從低到高表示
的系數(shù)。
輸入輸出樣例
輸入樣例
1 2
1 2
1 2 1
輸出樣例
1 4 5 2
問題分析
假設(shè)兩個(gè)多項(xiàng)式和
,兩個(gè)多項(xiàng)式可以寫作:
傳統(tǒng)方法是利用兩個(gè)多項(xiàng)式的系數(shù)進(jìn)行卷積運(yùn)算,得到一個(gè)次多項(xiàng)式
:
這種卷積運(yùn)算的時(shí)間復(fù)雜度為,顯然在數(shù)據(jù)范圍較大的情況下難以承受,而利用快速傅里葉變換可將時(shí)間復(fù)雜度降為
。
FFT介紹
快速傅里葉變換 (fast Fourier transform),即利用計(jì)算機(jī)計(jì)算離散傅里葉變換(DFT)的高效、快速計(jì)算方法的統(tǒng)稱,簡稱FFT??焖俑道锶~變換是1965年由J.W.庫利和T.W.圖基提出的。采用這種算法能使計(jì)算機(jī)計(jì)算離散傅里葉變換所需要的乘法次數(shù)大為減少,特別是被變換的抽樣點(diǎn)數(shù)N越多,F(xiàn)FT算法計(jì)算量的節(jié)省就越顯著。
FFT(Fast Fourier Transformation) 是離散傅氏變換(DFT)的快速算法。即為快速傅氏變換。它是根據(jù)離散傅氏變換的奇、偶、虛、實(shí)等特性,對離散傅立葉變換的算法進(jìn)行改進(jìn)獲得的。
——百度百科
要解決的問題是多項(xiàng)式的乘法,而一個(gè)多項(xiàng)式的表示方法并不唯一,傳統(tǒng)意義上多項(xiàng)式的表示利用的是系數(shù)表示法,即對于一個(gè)多項(xiàng)式可由一個(gè)系數(shù)向量
唯一表示。
而除了系數(shù)表示法之外,多項(xiàng)式也可以利用點(diǎn)值表示,對于多項(xiàng)式,選定
個(gè)
值
帶入多項(xiàng)式進(jìn)行計(jì)算,得到
個(gè)點(diǎn)值
這
個(gè)點(diǎn)值也可唯一表示多項(xiàng)式
。
因此當(dāng)我們利用同一個(gè)向量得到了兩個(gè)兩個(gè)多項(xiàng)式的點(diǎn)值表示法后,用對應(yīng)點(diǎn)值相乘,得到
即為兩個(gè)多項(xiàng)式的乘積多項(xiàng)式的點(diǎn)值表示,這個(gè)過程的時(shí)間復(fù)雜度為
。
需要注意的一點(diǎn)是,當(dāng)兩個(gè)多項(xiàng)式次數(shù)為時(shí),他們的乘積多項(xiàng)式次數(shù)為
,因此利用點(diǎn)值表示計(jì)算時(shí),計(jì)算兩個(gè)多項(xiàng)式的點(diǎn)值表示時(shí)應(yīng)選用
個(gè)變量,才能使得到的結(jié)果唯一表示乘積多項(xiàng)式
。
然而對于一個(gè)多項(xiàng)式來說,代入任意選定的變量
計(jì)算他的點(diǎn)值表示法時(shí)間復(fù)雜度依然是
,并沒有起到優(yōu)化的效果,而快速傅里葉變換解決了這個(gè)問題,使得系數(shù)表示法轉(zhuǎn)化為點(diǎn)值表示法的時(shí)間復(fù)雜度降低為
。
快速傅里葉變換FFT
FFT在計(jì)算多項(xiàng)式的系數(shù)表示法變換為點(diǎn)值表示法時(shí),選定復(fù)平面上單位圓上的單位復(fù)根作為變量計(jì)算多項(xiàng)式的點(diǎn)值,在這里單位根
滿足以下的一些性質(zhì)(如果有不理解的可以自行查閱復(fù)數(shù)的一些相關(guān)知識):
以上的這些性質(zhì)都可以由Euler公式得到,推導(dǎo)過程并非FFT重點(diǎn)這里就省略了。
對于一個(gè)多項(xiàng)式,我們可以對其進(jìn)行劃分,將偶數(shù)次項(xiàng)與奇數(shù)次項(xiàng)分開,在這里假設(shè)
為奇數(shù),得到:
我們分別定義兩個(gè)多項(xiàng)式:
那么原多項(xiàng)式就可以表示為:
將代入上式:
將代入上式:
由此可以發(fā)現(xiàn),和
在計(jì)算的過程中只有一個(gè)符號不同,因此在進(jìn)行枚舉計(jì)算
時(shí)即可直接得到
的值,利用這種方法進(jìn)行分治,便可以將復(fù)雜度降至
。
逆傅里葉變換IFFT
利用上述的方法得到了乘積多項(xiàng)式的點(diǎn)值表示法,那么現(xiàn)在需要解決的問題時(shí)如何將點(diǎn)值表示法再轉(zhuǎn)換回系數(shù)表示法。
假設(shè)得到多項(xiàng)式的FFT點(diǎn)值表示為,其系數(shù)表示為
,根據(jù)FFT原理,
可如下表示:
取的共軛復(fù)數(shù)
,如下定義向量
:
那么由定義可以推導(dǎo)出如下的公式:
對于復(fù)平面上的單位根,有如下的性質(zhì):
因此可以得到:
因此,利用FFT得到了多項(xiàng)式的點(diǎn)值表示后只需要將變量換為原本選定的單位根的共軛復(fù)數(shù)再進(jìn)行一次FFT就能得到多項(xiàng)式的系數(shù)表示。
這里需要說明一個(gè)問題,我們以上的討論都是建立在為
的冪次的條件下的,那么當(dāng)
不是
的冪次時(shí)需要將
擴(kuò)大為大于
的最小的
的冪次,在進(jìn)行逆傅里葉變換時(shí),通過上述的推導(dǎo)可以發(fā)現(xiàn),當(dāng)我們計(jì)算的
中
的值大于原本的
時(shí),就不存在
的情況了,因此求得的
為
,表示該多項(xiàng)式的
次項(xiàng)系數(shù)為
。
代碼實(shí)現(xiàn)
下面是利用遞歸實(shí)現(xiàn)FFT的代碼,具體的解釋見代碼注釋好了。
#include<bits/stdc++.h>
#define rep(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=1e6+10;
const double Pi=acos(-1.0);
int n,m,l=1;
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c<='9' && c>='0'){x=(x<<3)+(x<<1)+(int)(c-'0');c=getchar();}
return x*f;
}
struct Complex{
double re,im;
Complex (double xx=0,double yy=0){re=xx,im=yy;}
};
Complex operator + (Complex a,Complex b){
return Complex(a.re+b.re,a.im+b.im);
}
Complex operator - (Complex a,Complex b){
return Complex(a.re-b.re,a.im-b.im);
}
Complex operator * (Complex a,Complex b){
return Complex(a.re*b.re-a.im*b.im,a.im*b.re+a.re*b.im);
}
Complex a[N<<1],b[N<<1];
inline void FFT(Complex *a,int t,int inv){//inv表示當(dāng)前進(jìn)行FFT還是IFFT
if(t==1)return;//計(jì)算長度為1時(shí)回溯
int mid=t>>1;
static Complex tmp[N];
rep(i,0,mid-1)
tmp[i]=a[2*i],tmp[i+mid]=a[2*i+1];//將多項(xiàng)式按照次數(shù)奇偶進(jìn)行二分
rep(i,0,t-1)a[i]=tmp[i];
FFT(a,mid,inv);
FFT(a+mid,mid,inv);
Complex wn(cos(2.0*Pi/t),inv*sin(2.0*Pi/t)),w(1,0);//單位根
rep(i,0,mid-1){
tmp[i]=a[i]+w*a[i+mid];
tmp[i+mid]=a[i]-w*a[i+mid];
w=w*wn;
}
rep(i,0,t-1)a[i]=tmp[i];
}
int main(){
n=read(),m=read();
rep(i,0,n) a[i].re=read();
rep(i,0,m) b[i].re=read();
while(l<=(n+m))l<<=1;//找到大于等于n+m的最小2的冪次
FFT(a,l,1);
FFT(b,l,1);
rep(i,0,l)
a[i]=a[i]*b[i];
FFT(a,l,-1);
rep(i,0,n+m)
printf("%d ",(int)(a[i].re/l+0.5));//輸出整數(shù),需要注意精度
return 0;
}
但是問題到這里并沒有結(jié)束
當(dāng)有的毒瘤數(shù)據(jù)范圍非常大的時(shí)候,用遞歸進(jìn)行計(jì)算時(shí),大量的遞歸會造成棧溢出,那么是否有不用遞歸的做法?
FFT的迭代實(shí)現(xiàn)
對于這樣一個(gè)序列,我們觀察對其進(jìn)行二分的過程:

我們發(fā)現(xiàn)了一個(gè)神奇的性質(zhì),在對這個(gè)序列進(jìn)行二分以后的序列的二進(jìn)制可以由原序列的二進(jìn)制進(jìn)行翻轉(zhuǎn)得到,那么我們可以利用這個(gè)性質(zhì),用一個(gè)
Rader算法
Rader算法即為實(shí)現(xiàn)上述操作的一種算法,對于個(gè)數(shù),我們把遞增自然數(shù)
稱為順序數(shù)列;對順序數(shù)列中的每一個(gè)數(shù),將其二進(jìn)制倒序后轉(zhuǎn)化為十進(jìn)制,稱為倒序數(shù)列。
對于一個(gè)順序數(shù)列,第個(gè)數(shù)的二進(jìn)制可以視為將第
(這里是整除)個(gè)數(shù)的二進(jìn)制左移一位,再根據(jù)
的奇偶性對其末尾加
或者不加
。
那么要得到它的倒序數(shù)列,只需要將這個(gè)操作反向進(jìn)行即可,即第個(gè)數(shù)的二進(jìn)制可以視為將第
個(gè)數(shù)的二進(jìn)制右移一位,再根據(jù)
的奇偶性對其最高加
或者不加
,這里最高位即為第
位。
迭代進(jìn)行FFT(蝴蝶變換)
利用Rader算法求得了遞推序列,那么如何通過迭代得到最終的答案?
這其實(shí)跟迭代實(shí)現(xiàn)01背包的做法思路差不多,對于求在
次單位根的各冪次的點(diǎn)值時(shí),
次單位根的各冪次在
或
處的點(diǎn)值已經(jīng)被計(jì)算并且儲存在了
數(shù)組中,那么在下一層的迭代過程中直接使用
數(shù)組存儲的答案繼續(xù)進(jìn)行迭代計(jì)算即可。
迭代優(yōu)化FFT代碼實(shí)現(xiàn)
詳細(xì)解釋依舊見代碼注釋。
#include<bits/stdc++.h>
#define rep(i,a,b) for(register int i=a;i<=b;++i)
using namespace std;
const int N=1e7+10;
const double Pi=acos(-1.0);
int n,m,l=1,t=0;
int bin[N<<1];
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0' || c>'9'){if(c=='-')f=-1;c=getchar();}
while(c<='9' && c>='0'){x=(x<<3)+(x<<1)+(int)(c-'0');c=getchar();}
return x*f;
}
struct Complex{
double re,im;
Complex (double xx=0,double yy=0){re=xx,im=yy;}
};
Complex operator + (Complex a,Complex b){
return Complex(a.re+b.re,a.im+b.im);
}
Complex operator - (Complex a,Complex b){
return Complex(a.re-b.re,a.im-b.im);
}
Complex operator * (Complex a,Complex b){
return Complex(a.re*b.re-a.im*b.im,a.im*b.re+a.re*b.im);
}
Complex a[N<<1],b[N<<1];
inline void FFT(Complex *A,int inv){
rep(i,0,l-1)
if(i<bin[i]) swap(A[i],A[bin[i]]);//得到倒序數(shù)列
for(int mid=1;mid<l;mid<<=1){//枚舉當(dāng)前迭代區(qū)間的中點(diǎn)
Complex wn(cos(Pi/mid),inv*sin(Pi/mid));//單位根
for(int r=mid<<1,j=0;j<l;j+=r){//j枚舉迭代區(qū)間位置,用r來得到區(qū)間右端點(diǎn)
Complex w(1,0);
rep(k,0,mid-1){//k枚舉當(dāng)前迭代區(qū)間
Complex x=A[k+j],y=w*A[k+j+mid];
A[k+j]=x+y;
A[k+j+mid]=x-y;
w=w*wn;
}
}
}
}
int main(){
n=read(),m=read();
rep(i,0,n) a[i].re=read();
rep(i,0,m) b[i].re=read();
while(l<=(n+m)){
l<<=1,t++;
}
rep(i,0,l)//Rader算法
bin[i]=(bin[i>>1]>>1)|((i&1)<<(t-1));
FFT(a,1);FFT(b,1);
rep(i,0,l)
a[i]=a[i]*b[i];
FFT(a,-1);
rep(i,0,n+m)
printf("%d ",(int)(a[i].re/l+0.5));
return 0;
}