Fast Fourier Transportation(FFT)
·多項式的表達
係數表達
對於一個次數界爲n的多項式A(x)=∑j=0n−1ajxj而言,其係數表達是由一個係數組成的向量a=(a0,a1,...,an−1)。
點值表達
一個次數界爲n的多項式A(x)的點值表達就是一個由n個點值對所組成的集合
(x0,y0),(x1,y1),...,(xn−1,yn−1)
使得對k=0,1,…,n-1,所有xk各不相同,
yk=A(xk)
簡單的求點值運算我們可以隨意代入n個不相同的數,然後得出點對,時間複雜度Θ(n2)。後面可以看到,如果我們用一點巧妙的取值,可以使時間複雜度優化到Θ(nlg2n)。
求值計算的逆(從一個多項式的點值表達確定的係數表達形式)稱爲插值。
·多項式運算
Cj=k=0∑jAkBj−k
上述是多項式的乘法,我們把C稱爲A和B的卷積(convolution),表示成C=A⨂B。
FFT的主要思路是首先把A和B轉成點值表達,然後得到C的點值表達,再逆着做一遍,得到C的係數表達。
·DFT與FFT
上述做法太慢,我們要用Θ(n2)的時間把每個多項式轉成點值表達,然後再用Θ(n2)的時間轉回來,明顯很慢,還不如暴力。
我們想,可不可以使用某些特殊的數,使得每次可以做一次運算就可以得到多個數的呢?答案是有的:單位根複數根
n次單位複數根是滿足ωn=1的複數ω。n次單位複數根恰好有n個,對於k=0,1,…,n-1,這些根是eπik/n。爲了解釋這個表達式,我們用複數的指數形式來定義:
eiu=cos(u)+isin(u)
也就是給定一個單位圓,上面均勻地分佈着n個向量,如圖:
·關於n次單位復根
以上圖爲例我們可以發現,每一個n(這裏是8)次單位復根都是一個向量,他們在乘法意義下形成一個羣。
引理1:(消去引理)
對於任意整數n⩾0,k⩾0,以及d>0,
ωdkdk=ωnk
證明
ωdkdk=(e2πi/dn)dk=(e2πi/n)k=ωnk
引理2:(折半引理)
如果n>0爲偶數,那麼n個n次單位根的平方集合就是n/2個n/2次單位根的集合
證明
(ωnk+n/2)2=ωn2k+n=ωn2kωnn=(ωnk)2
因此ωnk與ωnk+n/2平方相等。
引理3:(求和引理)
對於任意整數n⩾0和不能被n整除的非負整數k,有
j=0∑n−1(ωnk)j=0
證明
j=0∑n−1(ωnk)j=1−ωnk(ωnk)0(1−(ωnk)n)=ωnk−1(ωnn)k−1=ωnk−1(1)k−1=0
因爲要求k不能被n整除,而且僅當k被n整除時ωnk=1成立,同時保證分母不爲0。
DFT
回顧一下,我們希望計算次數界爲n的多項式
A(x)=j=0∑n−1ajxj
在ωn0,ωn1,...,ωnn−1處的值。假設A以係數形式給出,接下來定義結果yk:
yk=A(ωnk)=j=0∑n−1ajωnkj
向量y=(y0,y1,...,yn−1)就是係數向量a=(a0,a1,...,an−1)的離散傅里葉變換(DFT)。我們也記作y=DFTn(a)。
FFT
通過使用一種稱爲快速傅里葉變換(FFT)的方法,利用復根的特殊性質,我們就可以在Θ(nlgn)的時間內計算出DFTn(a)。
注意:通篇的n我們假設是2的整數次冪。
FFT利用分治策略,採用A(x)中偶數下標的係數與奇數下標的係數,分別定義兩個新的次數界爲n/2的多項式
A[0](x)=a0+a2x+a4x2+...+an−2xn/2−1
A[1](x)=a1+a3x+a5x2+...+an−1xn/2−1
可以很容易發現
A(x)=A[0](x2)+xA[1](x2)
所以原問題轉化爲求兩個次數界爲n/2的多項式A[0](x)和A[1](x)在點(ωn0)2,(ωn1)2,...,(ωnn−1)2的取值。
所以我們可以發現在求出A[0](x2)和A[1](x2)以後,可以算出兩個復根的結果:
yk=yk[0]+ωnkyk[1]=A[0](ωn2k)+ωnkA[1](ωn2k)=A(ωnk)
還有
yk+(n/2)=yk[0]−ωnkyk[1]=yk[0]+ωnk+(n/2)yk[1]=A[0](ωn2k)+ωk+(n/2)A[1](ωn2k)
=A[0](ωn2k+n)+ωk+(n/2)A[1](ωn2k+n)=A(ωnk+(n/2))
所以就有代碼:
void FFT(comp *a,int n,int inv){
if(n==1) return;
int mid=n/2;
for (int i=0;i<mid;++i) c[i]=a[i*2],c[i+mid]=a[i*2+1];
for (int i=0;i<n;++i) a[i]=c[i];
FFT(a,mid,inv);
FFT(a+mid,mid,inv);
comp wn={cos(2.0*pi/n),inv*sin(2.0*pi/n)},w={1,0};
for (int i=0;i<mid;++i,w=w*wn){
c[i]=a[i]+w*a[i+mid];
c[i+mid]=a[i]-w*a[i+mid];
}
for (int i=0;i<n;++i) a[i]=c[i];
}
·在單位複數根的插值
現在我們展示如何在單位複數根處插值來完成多項式乘法方案,使得我們把一個多項式從點值表達轉換回係數表達。
我們可以把DFT寫成矩陣乘積
⎣⎢⎢⎢⎢⎢⎡y0y1y2⋮yn−1⎦⎥⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡1111⋮11ωnωn2ωn3⋮ωnn−11ωn2ωn4ωn6⋮ωn2(n−1)1ωn3ωn6ωn9⋮ωn3(n−1)⋯⋯⋯⋯⋱⋯1ωnn−1ωn2(n−1)ωn3(n−1)⋮ωn(n−1)(n−1)⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤⎣⎢⎢⎢⎢⎢⎡a0a1a2⋮an−1⎦⎥⎥⎥⎥⎥⎤
尷尬的是跑得賊慢:
隨便卡卡就爆了…
分治難免遞歸,遞歸常數大。
於是,考慮改進。
·蝴蝶變換
盜圖一張
可以發現,每個下標的二進制形式反過來就是它們最後在序列中的位置。
於是就有了迭代打法。
void FFT(Moon *a,int inv){
int i,j,len;
for (i=0;i<n;++i)
if(i<R[i])swap(a[i],a[R[i]]);
for (len=2;len<=n;len<<=1){
int half=len/2;
Moon w,wn={cos(Pi/half),inv*sin(Pi/half)};
for (j=0;j<n-i;j+=len,w={1,0}){
for (i=0;i<half;++i,w=w*wn){
Moon q=w*a[j+half+i],Q=a[j+i];
a[j+half+i]=Q-q;
a[j+i]=Q+q;
}
}
}
}
int main(){
for (i=0;i<n;++i) R[i]=(R[i>>1]>>1)|((i&1)<<(p-1));
}