「算法筆記」快速傅里葉變換(FFT)


一、引入

首先,定義多項式的形式為 \(f(x)=\sum_{i=0}^n a_ix^i\),其中 \(a_i\) 為系數,\(n\) 為次數,這種表示方法稱為“系數表示法”,一個多項式是由其系數確定的。

可以證明,\(n+1\) 個點可以唯一確定一個 \(n\) 次多項式。對於 \(f(x)\),代入 \(n+1\) 個不同的 \(x\),得到 \(n+1\) 個不同的 \(y\)。一個 \(n\) 次的多項式就可以等價地換成 \(n+1\) 個等式,相當於平面上的 \(n+1\) 組坐標 \((x_i,y_i)\),這種表示方法稱為“點值表示法”

多項式乘法 (卷積):\(C(x)=A(x)\cdot B(x)\)\(A,B,C\) 的系數構成的數列分別為 \(a,b,c\),則 \(c_k=\sum_{i=0}^ka_ib_{k-i}\)。理解:因為 \(x^i\times x^{k-i}=x^k\)\(a_i\)\(b_{k-i}\) 相乘后,它們后面的未知數就變成了 \(x^k\),對 \(c_k\) 產生貢獻。

暴力求解:兩個 \(n\) 次多項式相乘,時間復雜度 \(\mathcal{O}(n^2)\)

快速傅里葉變換(Fast Fourier Transform,簡稱 FFT)可以 \(\mathcal{O}(n\log n)\) 求解。

二、基本步驟

利用點值表示法,分三步快速求出多項式乘積:

  1. 由系數表示法轉換成點值表示法。(DFT)
  2. 利用點值表示法,求兩個多項式的乘積。
  3. 再將點值表示法轉化成系數表示法。(IDFT)

對於步驟二,給出兩個 \(n\) 次多項式 \(A\)\(B\) 的點值表達式,我們可以 \(\mathcal{O}(n)\) 求出其乘積 \(C\) 的點值表達式。顯然 \(C\)\(2n\) 次的,取 \(2n+1\) 個不同的 \(x_i\)

  • 代入 \(A\)\(\{(x_0,y_0),(x_1,y_1),\cdots,(x_{2n},y_{2n})\}\)
  • 代入 \(B\)\(\{(x_0,y'_0),(x_1,y'_1),\cdots,(x_{2n},y'_{2n})\}\)
  • \(C\) 的點值表達式為:\(\{(x_0,y_0y'_0),(x_1,y_1y'_1),\cdots,(x_{2n},y_{2n}y'_{2n})\}\)

下面重點分析步驟一。有一些前置概念。

三、復數

我們把形如 \(z=a+bi\)\(a,b\) 為實數)的數稱為 復數,其中,\(a,b\) 分別叫做復數 \(z\) 的實部與虛部,\(i\) 為虛數單位,\(i^2=-1\)

我們把復數表示在 復平面 上(\(x\) 軸叫實軸,\(y\) 軸叫虛軸),就像把實數表示在數軸上。如圖,復數 \(z=a+bi\) 與復平面內的點 \(Z(a,b)\) 一一對應。

連接 \(OZ\)。復數 \(z=a+bi\) 與平面向量 \(\vec{OZ}\) 一一對應。

復數的四則運算:

  • 加減法:\((a+bi)\pm (c+di)=(a\pm c)+(b\pm d)i\)
  • 乘法:\((a+bi)(c+di)=ac+adi+bci+bdi^2=(ac-bd)+(bc+ad)i\)
  • 除法:\(\frac{a+bi}{c+di}=\frac{(a+bi)\times (c-di)}{(c+di)\times(c-di)}=\frac{(ac+bd)+(bc-ad)i}{c^2+d^2}=\frac{ac+bd}{c^2+d^2}+\frac{bc-ad}{c^2+d^2}i\)
complex<double>a;    //STL。a.real() 返回復數 a 的實部,a.imag() 返回復數 a 的虛部 
struct cp{    //手寫,比 STL 快一點
    double a,b;
    cp operator+(cp &x){return (cp){a+x.a,b+x.b};} 
    cp operator-(cp &x){return (cp){a-x.a,b-x.b};}
    cp operator*(cp &x){return (cp){a*x.a-b*x.b,a*x.b+b*x.a};} 
    cp operator/(cp &x){double v=x.a*x.a+x.b*x.b; return (cp){(a*x.a+b*x.b)/v,(b*x.a-a*x.b)/v};}    //除法一般不用 qwq
};

我們稱 \(\overline{z}=a-bi\) 為復數 \(z=a+bi\)共軛復數

對於復數 \(z=a+bi\)模長 \(|z|\) 為點 \(Z(a,b)\) 到原點的距離,幅角 為對應的向量 \(\vec{OZ}\) 與橫軸正半軸的夾角。

復數的乘法可以表達為:模長相乘,輻角相加

四、單位根

1. 定義

\(n\) 次單位根是滿足 \(x^n=1\) 的復數 \(x\)

首先,單位根的模長必然為 \(1\)。因為若 \(|x|>1\),則 \(|x^n|=|x|^n>1\);若 \(|x|<1\),則 \(|x^n|=|x|^n<1\)

所以單位根表示的點一定在 單位圓(圓心為原點,半徑為 \(1\))上。

其次,單位根的輻角 \(\theta\) 一定滿足 \(\frac{n\theta}{2\pi}\in\mathbb{Z}\)。也就是一個向量從 \((1,0)\) 開始,每次旋轉 \(\theta\) 的角度,旋轉 \(n\) 次后還落在 \((1,0)\) 上,那么它一定旋轉了整數圈。

然后發現 \(n\) 次單位根正好是模長為 \(1\),輻角為 \(\frac{2k\pi}{n}\) 的向量對應的復數。

記模長為 \(1\),輻角為 \(\frac{2k\pi}{n}\) 的向量對應的 \(n\) 次單位根為 \(\omega_n^k\),稱為第 \(k\)\(n\) 次單位根。

還能發現,\(\omega_n^k=\omega_n^{k\bmod n}\),所以一般情況下,我們認為 \(n\) 次單位根有 \(n\) 個,即 \(\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}\)

2. 性質

單位根的性質:(這些性質在后文會被用到)

  • 性質 1:\(n\) 次單位根對應的向量將單位圓 \(n\) 等分。
    兩個相鄰的 \(n\) 次單位根對應的向量的夾角相等。單位根的輻角是周角的 \(\frac{1}{n}\)

  • 性質 2:\(\omega_n^k=\omega_n^{k\bmod n}\)
    在弧度制下,任意弧度 \(\theta\)\(\theta+2k\pi\,(k\in\mathbb{Z})\) 表示相同的角。

  • 性質 3:\({(\omega_n^k)}^p=\omega_n^{kp}\)
    \(k\)\(n\) 次單位根對應向量的輻角變為原來的 \(p\) 倍,相當於 \(\omega_n^{kp}\) 對應的向量。

  • 性質 4:\(\omega_{dn}^{dk}=\omega_{n}^k\)
    考慮兩者對應向量的輻角,\(\frac{2dk\pi}{dn}=\frac{2k\pi}{n}\)。也可以這樣理解:\(dn\) 次單位根對應的向量將單位圓 \(dn\) 等分,取第 \(dk\) 個。\(n\) 次單位根對應的向量將單位圓 \(n\) 等分,取第 \(k\) 個。兩者等價。

  • 性質 5:\(\omega_n^{k+n/2}=-\omega_n^k\)。其中 \(n\) 為偶數。
    相當於一個復數對應的向量進行一次中心對稱,\(a+bi\) 變為 \(-a-bi\)

根據性質 3 有,\({(\omega_n^k)}^2=\omega_{n}^{2k}\)。根據性質 4 有,\(\omega_n^{2k}=\omega_{n/2}^k\),其中 \(n\) 為偶數。

3. 求法

根據性質 3,有 \(\omega_n^k=(\omega_n^1)^k\)。也就是說,只要求出 \(\omega_n^1\),就能得到 \(\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}\)

\(\omega_n^1\) 所對應的向量模長為 \(1\),輻角為 \(\frac{2\pi}{n}\),得到 \(\omega_n^1\) 所對應的點為 \((\cos(\frac{2\pi}{n}),\sin(\frac{2\pi}{n}))\)

\(\pi\)double pi=acos(-1)

補充:\(\omega_n^k=e^{\frac{2\pi ik}{n}}=\cos(\frac{2\pi k}{n})+i\cdot \sin(\frac{2\pi k}{n})\)。其中 \(i\) 為虛數單位。

五、DFT

將系數表示法轉換成點值表示法。

1. 基本思路

對於 \(n-1\) 次多項式(也就是有 \(n\) 項) \(f(x)=\sum_{i=0}^{n-1} a_ix^i\),將奇偶次數分離。

\(f(x)=(a_0+a_2x^2+a_4x^4+\cdots+a_{n-2}x^{n-2})+(a_1x+a_3x^3+a_5x^5+\cdots+a_{n-1}x^{n-1})\)

定義兩個新的多項式 \(f_1(x)\)\(f_2(x)\)

  • \(f_1(x)=a_0+a_2x+a_4x^2+\cdots+a_{n-2}x^{{n/2-1}}\)
  • \(f_2(x)=a_1+a_3x+a_5x^2+\cdots+a_{n-1}x^{n/2-1}\)

於是有 \(f(x)=f_1(x^2)+xf_2(x^2)\)

\(\omega_n^k\,(k<\frac{n}{2})\) 代入得:\(f(\omega_n^k)=f_1(\omega_n^{2k})+\omega_n^kf_2(\omega_n^{2k})=f_1(\omega_{n/2}^k)+\omega_n^kf_2(\omega_{n/2}^k)\)

同理,將 \(\omega_n^{k+n/2}\,(k<\frac{n}{2})\) 代入得:\(f(\omega_n^{k+n/2})=f_1(\omega_n^{2k+n})+\omega_n^{k+n/2}f_2(\omega_n^{2k+n})\)
\(=f_1(\omega_n^{2k})+\omega_n^{k+n/2}f_2(\omega_n^{2k})=f_1(\omega_{n/2}^k)+\omega_n^{k+n/2}f_2(\omega_{n/2}^k)=f_1(\omega_{n/2}^k)-\omega_n^kf_2(\omega_{n/2}^k)\)

發現兩者的右邊只有正負號的區別。

第一個式子的 \(k\) 取遍 \([0,\frac{n}{2}-1]\) 時,\(k+\frac{n}{2}\) 取遍 \([\frac{n}{2},n-1]\)

如果我們知道 \(f_1(x),f_2(x)\) 分別在 \(x=\omega_{n/2}^0,\omega_{n/2}^1,\cdots,\omega_{n/2}^{n/2-1}\) 的點值表示,就可以 \(\mathcal{O}(n)\) 求出 \(f(x)\)\(x=\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}\) 的點值表示。

然后,發現 \(f_1(x),f_2(x)\)\(f(x)\) 的性質完全相同,這樣就把問題分成了兩個子問題,對於這兩個子問題再進行遞歸求解。這樣就可以在 \(\mathcal{O}(n\log n)\) 的時間復雜度內求出點值表達式。

2. 代碼實現

DFT 是利用單位根的特殊性質進行分治。

考慮到它能處理的多項式長度只能為 \(2^k\)\(k\) 為整數),否則在分治時左右的項數就會不同。我們可以在最高次補一些系數為 \(0\) 的項,把原來的 \(n\) 補到 \(2^k\,(2^k\geq n)\)。這樣不影響計算結果。

別忘了:\(f(\omega_n^k)=f_1(\omega_{n/2}^k)+\omega_n^kf_2(\omega_{n/2}^k),f(\omega_n^{k+n/2})=f_1(\omega_{n/2}^k)-\omega_n^kf_2(\omega_{n/2}^k)\)

遞歸實現 DFT:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,len;
double x,pi=acos(-1);
complex<double>a[N];
void DFT(complex<double>*a,int n){
    if(n==1) return ;    //邊界
    int m=n/2;
    complex<double>a1[m],a2[m];
    for(int i=0;i<m;i++) a1[i]=a[i<<1],a2[i]=a[i<<1|1];    //按奇偶分類 
    DFT(a1,m),DFT(a2,m);    //處理子問題
    complex<double>x(cos(2*pi/n),sin(2*pi/n)),w(1,0);    //x: w_n^1。w: 當前的 n 次單位根,初始值為 w_n^0,即 1。 
    for(int i=0;i<m;i++)
        a[i]=a1[i]+w*a2[i],a[i+m]=a1[i]-w*a2[i],w*=x;    //w*=x: 得到下一個單位根。 
} 
signed main(){
    scanf("%lld",&n);
    for(int i=0;i<n;i++)
        scanf("%lf",&x),a[i]=x;    //a[i] 的實部賦值為 x 
    for(len=1;len<n;len<<=1);    //把項數補到 2 的整冪,高次項的系數默認為 0 
    DFT(a,len);
    for(int i=0;i<len;i++)
        printf("(%.4lf,%.4lf)\n",a[i].real(),a[i].imag());
    return 0;
}

六、IDFT

DFT 的逆運算。將點值表示法轉化成系數表示法。

結論:把 DFT 中的 \(\omega_n^1\) 換成它的共軛復數,即 \((\cos(\frac{2\pi}{n}),-\sin(\frac{2\pi}{n}))\),得到的系數再除以 \(n\) 即可。證明略。

可以把 IDFT 和 DFT 放在一起寫。

void FFT(complex<double>*a,int n,int opt){    //opt=1 為 DFT,opt=-1 為 IDFT
    if(n==1) return ;
    int m=n/2;
    complex<double>a1[m],a2[m];
    for(int i=0;i<m;i++) a1[i]=a[i<<1],a2[i]=a[i<<1|1];
    FFT(a1,m,opt),FFT(a2,m,opt);
    complex<double>x(cos(2*pi/n),sin(2*pi/n)*opt),w(1,0);
    for(int i=0;i<m;i++)
        a[i]=a1[i]+w*a2[i],a[i+m]=a1[i]-w*a2[i],w*=x; 
} 

七、迭代實現

目前的代碼如下。可以發現它的效率並不是很高。

//Luogu P3803
#include<bits/stdc++.h>
using namespace std;
const int N=3e6+5;
int n,m,len;
double x,pi=acos(-1);
complex<double>a[N],b[N];
void FFT(complex<double>*a,int n,int opt){    //opt=1/-1: DFT/IDFT
    if(n==1) return ;
    int m=n/2;
    complex<double>a1[m],a2[m];
    for(int i=0;i<m;i++) a1[i]=a[i<<1],a2[i]=a[i<<1|1];
    FFT(a1,m,opt),FFT(a2,m,opt);
    complex<double>x(cos(2*pi/n),sin(2*pi/n)*opt),w(1,0);
    for(int i=0;i<m;i++)
        a[i]=a1[i]+w*a2[i],a[i+m]=a1[i]-w*a2[i],w*=x;     //蝴蝶操作(只是一個名字 qwq)。這里 w*a2[i] 算了兩次,先記錄下來再算可以減小常數。
} 
signed main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n;i++)
        scanf("%lf",&x),a[i]=x;
    for(int i=0;i<=m;i++)
        scanf("%lf",&x),b[i]=x;
    n=n+m+1;    //一個 n 次多項式和一個 m 次多項式的乘積是一個 n+m 次多項式 (有 n+m+1 項)
    for(len=1;len<n;len<<=1); 
    FFT(a,len,1),FFT(b,len,1);
    for(int i=0;i<len;i++) a[i]*=b[i];    //點值直接乘
    FFT(a,len,-1);
    for(int i=0;i<n;i++)
        printf("%d%c",(int)(a[i].real()/len+0.5),i==n-1?'\n':' ');    //注意這里是除以 len
    return 0;
}

如圖,考慮遞歸的結構:

求出第 \(4\) 層的狀態,就能往上合並求出前 \(3\) 層的狀態。

觀察發現,最后的數組下標的序列是原序列的 二進制翻轉。比如 \(6=(110)_2\),反過來就是 \((011)_2=3\)。而第 \(4\)\(a_3\) 的位置就是原來 \(a_6\) 的位置。

//r[i] 表示 i 二進制翻轉后的結果。求 0~len-1 在二進制位數為 log2(len)-1 意義下的二進制翻轉。
for(int i=0;i<len;i++)    //len 是 2 的冪次 
    r[i]=(r[i>>1]>>1)|((i&1)?len>>1:0);

理解:考慮 \(i\)\(\frac{i}{2}\) 在二進制下的關系。\(i\) 可以看作是 \(\frac{i}{2}\) 在二進制下的每一位左移一位得到。翻轉后,\(i\)\(\frac{i}{2}\) 在二進制下的每一位右移一位得到,然后判一下最后一位即可。

迭代實現:

//Luogu P3803
#include<bits/stdc++.h>
using namespace std;
const int N=3e6+5;
int n,m,len,r[N];
double x,pi=acos(-1);
complex<double>a[N],b[N];
void FFT(complex<double>*a,int n,int opt){    //opt=1/-1: DFT/IDFT
    for(int i=0;i<n;i++) 
        if(i<r[i]) swap(a[i],a[r[i]]);    //求出最后一層的序列 
    for(int k=2;k<=n;k<<=1){    //枚舉區間長度 
        int m=k>>1;     //待合並的長度 
        complex<double>x(cos(2*pi/k),sin(2*pi/k)*opt),w(1,0),v;
        for(int i=0;i<n;i+=k,w=1)    //枚舉起始點 
            for(int j=i;j<i+m;j++)    //遍歷區間
                v=w*a[j+m],a[j+m]=a[j]-v,a[j]=a[j]+v,w*=x;    //蝴蝶操作。注意先 a[j+m]=a[j]-v 再 a[j]=a[j]+v。
    }
} 
signed main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n;i++)
        scanf("%lf",&x),a[i]=x;
    for(int i=0;i<=m;i++)
        scanf("%lf",&x),b[i]=x;
    n=n+m+1;
    for(len=1;len<n;len<<=1); 
    for(int i=0;i<len;i++)    //二進制翻轉 
        r[i]=(r[i>>1]>>1)|((i&1)?len>>1:0);
    FFT(a,len,1),FFT(b,len,1);
    for(int i=0;i<len;i++) a[i]*=b[i];
    FFT(a,len,-1);
    for(int i=0;i<n;i++)
        printf("%d%c",(int)(a[i].real()/len+0.5),i==n-1?'\n':' ');
    return 0;
}

八、模板

P3803 【模板】多項式乘法(FFT) 為例。

遞歸實現:

#include<bits/stdc++.h>
using namespace std;
const int N=3e6+5;
int n,m,len;
double x,pi=acos(-1);
complex<double>a[N],b[N];
void FFT(complex<double>*a,int n,int opt){    //opt=1/-1: DFT/IDFT
    if(n==1) return ;
    int m=n/2;
    complex<double>a1[m],a2[m];
    for(int i=0;i<m;i++) a1[i]=a[i<<1],a2[i]=a[i<<1|1];
    FFT(a1,m,opt),FFT(a2,m,opt);
    complex<double>x(cos(2*pi/n),sin(2*pi/n)*opt),w(1,0);
    for(int i=0;i<m;i++)
        a[i]=a1[i]+w*a2[i],a[i+m]=a1[i]-w*a2[i],w*=x; 
} 
signed main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n;i++)
        scanf("%lf",&x),a[i]=x;
    for(int i=0;i<=m;i++)
        scanf("%lf",&x),b[i]=x;
    n=n+m+1;
    for(len=1;len<n;len<<=1); 
    FFT(a,len,1),FFT(b,len,1);
    for(int i=0;i<len;i++) a[i]*=b[i];
    FFT(a,len,-1);
    for(int i=0;i<n;i++)
        printf("%d%c",(int)(a[i].real()/len+0.5),i==n-1?'\n':' ');
    return 0;
}

迭代實現:(比遞歸快)

#include<bits/stdc++.h>
using namespace std;
const int N=3e6+5;
int n,m,len,r[N];
double x,pi=acos(-1);
complex<double>a[N],b[N];
void FFT(complex<double>*a,int n,int opt){    //opt=1/-1: DFT/IDFT
    for(int i=0;i<n;i++)
        if(i<r[i]) swap(a[i],a[r[i]]);
    for(int k=2;k<=n;k<<=1){
        int m=k>>1; 
        complex<double>x(cos(2*pi/k),sin(2*pi/k)*opt),w(1,0),v;
        for(int i=0;i<n;i+=k,w=1)
            for(int j=i;j<i+m;j++) v=w*a[j+m],a[j+m]=a[j]-v,a[j]=a[j]+v,w*=x;
    }
} 
signed main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n;i++)
        scanf("%lf",&x),a[i]=x;
    for(int i=0;i<=m;i++)
        scanf("%lf",&x),b[i]=x;
    n=n+m+1;
    for(len=1;len<n;len<<=1); 
    for(int i=0;i<len;i++)
        r[i]=(r[i>>1]>>1)|((i&1)?len>>1:0);
    FFT(a,len,1),FFT(b,len,1);
    for(int i=0;i<len;i++) a[i]*=b[i];
    FFT(a,len,-1);
    for(int i=0;i<n;i++)
        printf("%d%c",(int)(a[i].real()/len+0.5),i==n-1?'\n':' ');
    return 0;
}

記憶:

  • \(f(x)=f_1(x^2)+xf_2(x^2)\)
  • \(f(\omega_n^k)=f_1(\omega_{n/2}^k)+\omega_n^kf_2(\omega_{n/2}^k)\)
  • \(f(\omega_n^{k+n/2})=f_1(\omega_{n/2}^k)-\omega_n^kf_2(\omega_{n/2}^k)\)


免責聲明!

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



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