前言
\(\text{FFT}\)(快速傅里葉變換)是 \(O(n\log n)\) 解決多項式乘法的一個算法,\(\text{NTT}\)(快速數論變換)則是在模域下的,而 \(\text{MTT}\)(毛神仙對\(\text{FFT}\)的精度優化算法)可以針對任意模數。本文主要講解這三種算法,具體的應用還請參考我博客內的題解。
正文
FFT-快速傅里葉變換
學習這個算法可以借助《算法導論》,當然算導上的東西需要耐心才能啃下來。這里只是概括一下算導上的介紹,並加入一些個人的見解。下面逐步介紹這個算法。
復數
如果學過的話可以跳過。實數可以一一對應數軸上的點,那么復數就可以一一對應平面直角坐標系上的點。對應 \(x\) 軸上的點的就是我們熟悉的實數,而外面的就是虛數。其中 \((0,1)\) 這個點對應的數記作 \(i\) ,即 \(\sqrt{-1}\),它表示虛數單位。復數可以表示成 \(a+ib\) 的形式,其中 \(a,b\) 為實數。
極坐標表示法下的乘法
\((a,\alpha)\cdot(b,\beta)=(ab,\alpha+\beta)\)
證明如下:
代數表示法下的乘法
\((a+ib)\cdot(c+id)=ac-bd+i(ad+bc)\)
無需證明,肉眼化簡。
單位復數根
在單位圓上,我們用 \(\omega_{n}^k\) 表示將單位圓 \(n\) 等分,取其第 \(k\) 條線對應的單位復數。其中 \(\omega_n^0=1\) ,逆時針方向編號,如圖所示:
單位復根有一些重要的性質。
消去引理
\(\omega_{dn}^{dk}=\omega_{n}^k\) 其中 \(n,k\geq 0,d>0\)
折半引理
\((\omega_n^{k+n/2})^2=(\omega_n^k)^2\) 其中\(n\geq0,k\geq 0\)
如果借助向量去理解的話,理解起來非常方便。
多項式
一個形如 \(\displaystyle A(x)=\sum_{i=0}^{n-1}a_ix^i\) 的式子。
系數表示
直接列出 \(A(x)\) 的各項系數。這種表示方法可以 \(O(n)\) 的實現多項式加法,但多項式乘法卻需要 \(O(n^2)\)
點值表示
通過帶入若干個特值確定,顯然,一個最高次為 \(n-1\) 的多項式需要 \(n\) 的特殊值便唯一確定。這種表示方法可以 \(O(n)\) 的加和乘,但是要轉化成系數表示才能體現出它作為多項式的價值。
DFT
對於一個列向量 \(a=(a_0,a_1,\cdots,a_{n-1})\) ,以它為系數的多項式 \(A(x)=\displaystyle\sum_{j=0}^{n-1}a_jx^j\)
若有一個列向量 \(y=(y_0,y_1,\cdots,y_{n-1})\) 滿足 \(y_k=A(\omega_n^k)\) ,則\(y=\text{DFT}_n(a)\)
\(\text{DFT}\) 的全稱為離散傅里葉變換,是將多項式的系數表達化作點值表達的一個變換。
同理 \(a=\text{DFT}_n^{-1}(y)\) ,\(\text{DFT}^{-1}\) 就是逆離散傅里葉變換,也稱 \(\text{IDFT}\),我們嘗試寫出 \(\text{DFT}^{-1}\) 的表達式。
寫出 \(y\) 與 \(a\) 的關系
然后我們可以矩陣乘積 \(y=V_na\) 的形式表示向量 \(a\) 到向量 \(y\) 的變換。\(V_n\) 為由 \(\omega_n\) 各項指數構成的范德蒙德矩陣。
那我們現在要求的就是 \(V_n^{-1}\) 的矩陣,即 \(V_n\) 的逆矩陣。
有如下定理:
對於 \(j,k\in[0,n)\) ,\(V_n^{-1}\) \((j,k)\) 處的元素為 \(\omega_n^{-jk}/n\)
證明如下
顯然,當 \(j=j'\) 時,\([V_nV_n^{-1}]_{jj'}\) 的值為 \(1\) ,否則為 \(0\) ,那么 \([V_nV_n^{-1}]\) 是一個行列數為 \(n\) 的單位矩陣,即得證 \(V^{-1}\) 為 \(V\) 的逆矩陣。
那么在作 \(\text{IDFT}\) 的時候,只需將單位根換成 \({\omega_n^{-1}}\) ,最后系數再除以 \(n\) 即可。
當然,直接變換是 \(O(n^2)\) 的。我們考慮用分治的思想進行變換。
FFT
首先觀察多項式 \(A(x)\) ,我們將指數分奇偶兩類。偶數項以 \(\{a_0,a_2,...,a_{n-2}\}\) 構造一個新的多項式 \(\displaystyle A^{[0]}(x)=\sum_{j=0}^{n/2-1}a_{2j}x^j\),奇數項同理為 \(\displaystyle A^{[1]}(x)=\sum_{j=0}^{n/2-1}a_{2j+1}x^j\)。
那么顯然有
我們把 \(\omega_n^k\) 代入得到
利用消去引理得到
那么將 \(A^{[0]},A^{[1]}\) 的系數向量 \(a^{[0]},a^{[1]}\) 進行一次 \(\text{DFT}\) ,分別得到 \(y^{[0]},y^{[1]}\) 。
有
只要令 \(k<n/2\) ,將 \(k\geq n/2\) 的部分用折半引理即可。
推導不難,注意將在單位圓上的旋轉借用平面向量來理解。
用 \(y\) 代入,最終的表達式為
這樣就可以分治求解了。
更高效的FFT
事實上 \(\text{FFT}\) 可以迭代求解。先觀察一下遞歸求解的過程,如圖所示。
然后用人類智慧觀察,發現 \(a_i\) 在底層是在的位置為 \(i\) 的二進制位翻轉。
發現只需要枚舉區間長度,掃整個序列,就可以進行對區間進行合並。觀察遞歸求解的式子
它的流程可以用上圖來表示,上面操作叫作蝴蝶操作,其實和遞歸求解的流程相似。具體還是看代碼,碼風還是清晰的。
struct Complex
{
double x,y;
Complex operator +(const Complex &_){return (Complex){x+_.x,y+_.y};}
Complex operator -(const Complex &_){return (Complex){x-_.x,y-_.y};}
Complex operator *(const Complex &_){return (Complex){x*_.x-y*_.y,x*_.y+y*_.x};}
Complex operator /(const int &_){return (Complex){x/_,y/_};}
};
namespace _Polynomial
{
Complex A[N<<1],B[N<<1];
Complex w[N<<1];int r[N<<1];
void DFT(Complex *a,int op,int n)
{
FOR(i,0,n-1)if(i<r[i])swap(a[i],a[r[i]]); //位翻轉
for(int i=2;i<=n;i<<=1) //合並出一個長i的區間
for(int j=0;j<n;j+=i) //區間開頭的位置
for(int k=0;k<i/2;k++) //蝴蝶操作
{
Complex u=a[j+k],t=w[op==1?n/i*k:n-n/i*k]*a[j+k+i/2];
a[j+k]=u+t,a[j+k+i/2]=u-t;
}
if(op==-1)FOR(i,0,n-1)a[i]=a[i]/n;
}
void multiply(const int *a,const int *b,int *c,int n1,int n2)
{
int n=1;
while(n<n1+n2-1)n<<=1;
FOR(i,0,n1-1)A[i].x=a[i],A[i].y=0;
FOR(i,0,n2-1)B[i].x=b[i],B[i].y=0;
FOR(i,n1,n-1)A[i].x=A[i].y=0;
FOR(i,n2,n-1)B[i].x=B[i].y=0;
FOR(i,0,n-1)r[i]=(r[i>>1]>>1)|((i&1)*(n>>1));
FOR(i,0,n)w[i]=(Complex){cos(2*PI*i/n),sin(2*PI*i/n)};
DFT(A,1,n),DFT(B,1,n);
FOR(i,0,n-1)A[i]=A[i]*B[i];
DFT(A,-1,n);
FOR(i,0,n1+n2-2)c[i]=A[i].x+0.5;
}
};
顯而易見,由於 \(\text{double}\) 的存在,精度多多少少會被卡一點。而具體的題目經常往往會給一個特殊的模數,這種時候就要用到接下來介紹的算法了。
NTT-快速數論變換
待補充。。。