前言 關(guān)于為什么又開始在這個(gè)博客上寫算法
FFT是我高中學(xué)競(jìng)賽階段接觸到的最后一個(gè)算法隅肥,也是我高中一直沒有學(xué)會(huì)的算法之一乓序,在高中畢業(yè)的時(shí)候認(rèn)為自己大學(xué)本科一定學(xué)的是CS仙畦,所以想要把這些自己之前沒有學(xué)會(huì)的算法都再琢磨一下态坦,可是基于當(dāng)時(shí)的知識(shí)儲(chǔ)備我還完全不知道復(fù)數(shù)域和復(fù)平面以及多項(xiàng)式的各種定理,于是半途而廢了埂淮,F(xiàn)FT也成了在算法學(xué)習(xí)上我的一個(gè)心結(jié)之一姑隅。
但是巧合的是,我大學(xué)本科并沒有學(xué)CS倔撞,而是學(xué)了數(shù)學(xué)讲仰,這些我一直未曾學(xué)會(huì)的算法也就被一直擱置了兩年,直到昨天我在上數(shù)據(jù)結(jié)構(gòu)課時(shí)误窖,數(shù)學(xué)院數(shù)據(jù)結(jié)構(gòu)課所學(xué)內(nèi)容跟OI比起來簡(jiǎn)直不值一提叮盘,于是無聊的我突發(fā)奇想翻開了自己之前洛谷的提交記錄,發(fā)現(xiàn)FFT的版子還一直放在我的題單霹俺,于是就想利用這些時(shí)間把這些之前沒學(xué)懂的算法解決掉好了柔吼,也為明年考研跨考CS打下基礎(chǔ)。
這次在數(shù)學(xué)院歷練了兩年多的我拿著高等代數(shù)數(shù)學(xué)分析復(fù)變函數(shù)的知識(shí)再來看FFT丙唧,頓時(shí)覺得這些知識(shí)難度與我兩年前看的時(shí)候相比要簡(jiǎn)單了許多愈魏,在昨天晚上解決了自己所有的疑難點(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)稱习瑰,簡(jiǎn)稱FFT绪颖』嗷纾快速傅里葉變換是1965年由J.W.庫(kù)利和T.W.圖基提出的。采用這種算法能使計(jì)算機(jī)計(jì)算離散傅里葉變換所需要的乘法次數(shù)大為減少柠横,特別是被變換的抽樣點(diǎn)數(shù)N越多窃款,F(xiàn)FT算法計(jì)算量的節(jié)省就越顯著。
FFT(Fast Fourier Transformation) 是離散傅氏變換(DFT)的快速算法牍氛。即為快速傅氏變換晨继。它是根據(jù)離散傅氏變換的奇、偶搬俊、虛紊扬、實(shí)等特性,對(duì)離散傅立葉變換的算法進(jìn)行改進(jìn)獲得的唉擂。
——百度百科
要解決的問題是多項(xiàng)式的乘法餐屎,而一個(gè)多項(xiàng)式的表示方法并不唯一,傳統(tǒng)意義上多項(xiàng)式的表示利用的是系數(shù)表示法玩祟,即對(duì)于一個(gè)多項(xiàng)式可由一個(gè)系數(shù)向量
唯一表示腹缩。
而除了系數(shù)表示法之外,多項(xiàng)式也可以利用點(diǎn)值表示,對(duì)于多項(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)值表示法后盘寡,用對(duì)應(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)式
常潮。
然而對(duì)于一個(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)知識(shí)):
以上的這些性質(zhì)都可以由Euler公式得到,推導(dǎo)過程并非FFT重點(diǎn)這里就省略了献联。
對(duì)于一個(gè)多項(xiàng)式竖配,我們可以對(duì)其進(jìn)行劃分,將偶數(shù)次項(xiàng)與奇數(shù)次項(xiàng)分開里逆,在這里假設(shè)
為奇數(shù)进胯,得到:
我們分別定義兩個(gè)多項(xiàng)式:
那么原多項(xiàng)式就可以表示為:
將代入上式:
將代入上式:
由此可以發(fā)現(xiàn),和
在計(jì)算的過程中只有一個(gè)符號(hào)不同原押,因此在進(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)出如下的公式:
對(duì)于復(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ì)算長(zhǎng)度為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í)终畅,大量的遞歸會(huì)造成棧溢出,那么是否有不用遞歸的做法竟闪?
FFT的迭代實(shí)現(xiàn)
對(duì)于這樣一個(gè)序列离福,我們觀察對(duì)其進(jìn)行二分的過程:
我們發(fā)現(xiàn)了一個(gè)神奇的性質(zhì),在對(duì)這個(gè)序列進(jìn)行二分以后的序列的二進(jìn)制可以由原序列的二進(jìn)制進(jìn)行翻轉(zhuǎn)得到炼蛤,那么我們可以利用這個(gè)性質(zhì)妖爷,用一個(gè)
Rader算法
Rader算法即為實(shí)現(xiàn)上述操作的一種算法,對(duì)于個(gè)數(shù)暗挑,我們把遞增自然數(shù)
稱為順序數(shù)列笋除;對(duì)順序數(shù)列中的每一個(gè)數(shù)斜友,將其二進(jìn)制倒序后轉(zhuǎn)化為十進(jìn)制炸裆,稱為倒序數(shù)列。
對(duì)于一個(gè)順序數(shù)列鲜屏,第個(gè)數(shù)的二進(jìn)制可以視為將第
(這里是整除)個(gè)數(shù)的二進(jìn)制左移一位烹看,再根據(jù)
的奇偶性對(duì)其末尾加
或者不加
。
那么要得到它的倒序數(shù)列洛史,只需要將這個(gè)操作反向進(jìn)行即可惯殊,即第個(gè)數(shù)的二進(jìn)制可以視為將第
個(gè)數(shù)的二進(jìn)制右移一位,再根據(jù)
的奇偶性對(duì)其最高加
或者不加
也殖,這里最高位即為第
位土思。
迭代進(jìn)行FFT(蝴蝶變換)
利用Rader算法求得了遞推序列务热,那么如何通過迭代得到最終的答案?
這其實(shí)跟迭代實(shí)現(xiàn)01背包的做法思路差不多己儒,對(duì)于求在
次單位根的各冪次的點(diǎn)值時(shí)崎岂,
次單位根的各冪次在
或
處的點(diǎn)值已經(jīng)被計(jì)算并且儲(chǔ)存在了
數(shù)組中,那么在下一層的迭代過程中直接使用
數(shù)組存儲(chǔ)的答案繼續(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;
}