FFT 與 NTT

發表於 2021-01-22 00:00 2759 字 14 min read
先備知識 1. 生成函數 (Generating Function) 在做多項式乘法的時候,每一個多項式都可以視為一個集合,乘法就是從每個集合裡各自挑出一個元素,接著枚舉所有組合並合併、化簡。舉例來說,將多項式 $(1+x)(1+x+x^2)$ 展開,用上述的方法可將展開過程以樹狀圖表示如下: 而用直式乘法也可得 $(1+x)(1+x+x^2)=1+2x+2x^2+x^3$。...

先備知識

1. 生成函數 (Generating Function)

在做多項式乘法的時候,每一個多項式都可以視為一個集合,乘法就是從每個集合裡各自挑出一個元素,接著枚舉所有組合並合併、化簡。舉例來說,將多項式 (1+x)(1+x+x2)(1+x)(1+x+x^2) 展開,用上述的方法可將展開過程以樹狀圖表示如下:

而用直式乘法也可得 (1+x)(1+x+x2)=1+2x+2x2+x3(1+x)(1+x+x^2)=1+2x+2x^2+x^3

既然多項式乘法能夠轉化為計數問題了,組合問題當然也能夠轉化為多項式乘法,例如以下問題:現有三個元素 a,b,ba, b, b,求從中挑出 030~3 個元素的組合數有多少種?

aa 歸類為 AA 集合,兩個 bb 歸類為 BB 集合。對於 AA 集合來說,會發現 aa 只有不取(00)或取(11)兩種可能,而對於 BB 集合,會發現 bb 有不取(00)、取 1 個(11)或取 2 個(22)三種可能,觀察到可以將問題轉化成求 (1+x)(1+x+x2)(1+x)(1+x+x^2) 展開式,其中取 ii 個元素的組合數即為 (1+x)(1+x+x2)(1+x)(1+x+x^2)xix^i 項係數,記為 [xi](1+x)(1+x+x2)[x^i](1+x)(1+x+x^2)

像這種為一個問題產生一個多項式函數,並利用這個函數求解問題的技巧即為多項式生成函數。

2. 多項式插值唯一定理 (Uniqueness of Interpolating Polynomial)

平面上有 n+1n+1 個相異點 (x0,y0)(x_0, y_0)(x1,y1)(x_1, y_1)、……、(xn,yn)(x_n, y_n),則必定存在唯一一個多項式 P(x)P(x) 使得 0in, P(xi)=yi\forall 0 \leq i \leq n,\ P(x_i)=y_i,並且 P(x)P(x) 最高次項的次數 n\leq n,i.e. deg(P(x))ndeg(P(x)) \leq n

此定理的證明使用了范德蒙矩陣的性質(其行列式不為 00 等價於證明了唯一定理):維基百科 - 多項式插值

回想一下國中數學課所教的線型函數和二次函數,平面上兩個點便能決定唯一一條直線;同樣地對於二次函數來說,只需要在平面上找三個點便能知道這個二次函數長怎樣;這個定理告訴我們這些結論可以推廣到任意 nn 次多項式,而且也和接下來介紹的的傅立葉變換有關。

3. 離散傅立葉變換 (DFT, Discrete Fourier Transform)

多項式插值為一定理告訴我們,n+1n+1 個點便能表達唯一一個 nn 次多項式。舉例來說,x2+x+1x^2 + x + 1 便可用 {(0,1),(1,3),(2,7)}\{(0, 1), (1, 3), (2, 7)\} 這三個點來表示,像這樣以點表達多項式的方法便稱作點值表達法。

因此,將某 nn 次多項式 f(x)f(x) 從我們已經看膩的係數表達法轉換成點值表達法,便只要找出任意 n+1n+1xx ,代入多項式之後得到 yy,這 n+1n+1(x,y)(x, y) 便可代表 f(x)f(x) 了。上述將我們已經看膩的係數表達法轉換成點值表達法,就是離散傅立葉變換(DFT)

而將 n+1n+1 個點轉化為原本係數形式的多項式,可使用高一教的拉格朗日插值法或牛頓插值法還原,此步驟稱作 IDFT(DFT 的反運算)

你也許會問,為什麼要用點來表示多項式。回想一下平時計算多項式乘法 A×BA \times B 的過程,是不是都要 AA 的每一項和 BB 的每一項相乘,最後合併、化簡。但如果是平面上的點,計算乘法便只要把對應點的 yy 值相乘就行了!(但先決條件是表達 AA 的點的 xx 座標要和表達 BB 的點的 xx 座標一一對應)

具體來說,設 C=A×B, deg(A)=n, deg(B)=mC = A \times B,\ deg(A) = n,\ deg(B) = m,由於 deg(C)=n×mdeg(C) = n \times m,所以對 AABBDFT 時皆必須找 n×m+1n \times m + 1 個點,才能對 CCIDFT

A={( xi,A(xi) )  0inm}A = \{(\ x_i, A(x_i)\ )\ |\ 0 \leq i \leq nm\}

B={( xi,B(xi) )  0inm}B = \{(\ x_i, B(x_i)\ )\ |\ 0 \leq i \leq nm\}

C={( xi,A(xi)×B(xi) )  0inm}C = \{(\ x_i, A(x_i) \times B(x_i)\ )\ |\ 0 \leq i \leq nm\}

如此一來便能省去一直配對要花的時間。

總結來說,現在我們已經知道另外一種更快的方法做多項式乘法了。如果要計算 nn 次多項式 AA 乘上 mm 次多項式 BB ,則先找任意 nm+1nm+1xxAABB 轉換為點值表達法。接著做上述的乘法得到 CC ,最後將 CC 轉換為係數表達法便完成乘法了!

但關鍵還是卡在 DFTIDFT,因為還是要一個點一個點慢慢代入啊!這樣做還是不夠快(不會過這題),所以我們需要用到複數的一些性質加速轉換的步驟,也就是今天的主角——FFT

4. 單位根 (Root of Unity)

考慮以下方程式:ωn=1\omega^n=1,稱此方程式的所有解 ω\omegann 次單位根,記為 ωn\omega_n

nn 次單位根共有 nn 個,接著將這 nn 個單位根畫在複數平面上,會發現他們會落在單位圓上(半徑為 1,圓心位於 0+0i0+0i ),且會圍出一個正 nn 邊形,第一個點 ω0\omega_0 位於 1+0i1+0i ,從實數正軸開始逆時針旋轉,依序會經過 ω1\omega_1ω2\omega_2、...、 ωn1\omega_{n-1}

下圖由左至右依序為二次、三次、四次單位根。

根據這張圖,再由歐拉公式將這些根極坐標化,可得:

ωk=e2πikn,k=0,1,2,...,n1\omega^k = e^{\frac{2\pi ik}{n}}, k=0, 1, 2, ..., n-1

接著再觀察仔細一點,會發現 nn 為偶數時,任意 ωk\omega^k 對原點的對稱點皆為 ωk+n/2\omega^{k+n/2},意即 ωk=ωk+n/2\omega^k = -\omega^{k+n/2},這是個非常漂亮而且重要的性質!也是 FFT 之所以能那麼快的重要關鍵!

FFT (Fast Fourier Transform) 與 Cooley-Tukey 演算法

方便起見以下假設 nn22 的冪次。並且接下來介紹實作 FFT 的方法為常見的演算法—— Cooley-Tukey Algorithm

有了單位根的概念,接下來我們要想辦法優化 DFT,關鍵仍是卡在要決定 nnxx 座標並一一代入 n1n-1 次多項式 f(x)f(x) 尋找 yy 座標。想要短時間內就知道一個 xxf(x)f(x) 值似乎不太可能。於是我們把焦點轉向這個問題:能不能不要做那麼多次代入運算?也就是如果已經知道某個點 (xi,f(xi))(x_i, f(x_i)),能不能立刻產生出另外一個點 (xj,f(xj))(x_j, f(x_j))

可以!而且這件事情國中數學就已經提過了!觀察 f(x)=x2f(x) = x^2。如果已經知道 (3,9)(3, 9)f(x)f(x) 的圖形上,便可馬上知道 (3,9)(-3, 9) 也在 f(x)f(x) 上。(成功快速找出第二個點了!)

沿用這個概念,對於一個多項式 P(x)=a0+a1x+...an1xn1P(x)=a_0+a_1x+...a_{n-1}x^{n-1},先將其奇數項和偶數項拆開來並將奇數的那一組提出 xx,令偶數項那組為 Pe(x2)P_e(x^2),奇數項那組提出 xx 後為 Po(x2)P_o(x^2)

Pe(x2)=a0+a2x2+...an2xn2P_e(x^2)=a_0+a_2x^2+...a_{n-2}x^{n-2} Po(x2)=a1+a3x2+...an1xn2P_o(x^2)=a_1+a_3x^2+...a_{n-1}x^{n-2}

P(x)=Pe(x2)+xPo(x2)P(x)=P_e(x^2)+xP_o(x^2)

基於 Pe(x2)P_e(x^2)Po(x2)P_o(x^2)xx 的偶函數,利用正負成對的性質,這裡所選要代入的 xx±x1,±x2,...,±xn/2\pm x_1, \pm x_2, ..., \pm x_{n/2}。則對於每一對 ±xi\pm x_i 有以下推論:

P(xi)=Pe(xi2)+xiPo(xi2)P(x_i) = P_e(x_i^2)+x_iP_o(x_i^2) P(xi)=Pe(xi2)xiPo(xi2)P(-x_i) = P_e(x_i^2)-x_iP_o(x_i^2)

接下來的任務便是找到 n/2n/2 個點表示 Pe(x2)P_e(x^2)Po(x2)P_o(x^2)。這 n/2n/2 個點也已經被決定了——x12,x22,...xn/22x_1^2, x_2^2, ... x_{n/2}^2。發現了嗎,同樣的問題可以被繼續遞迴求解,這就是 FFT 的精髓!

注意到此作法還有一個很重要的問題,那就是如何決定適合的 xx,使得 x12,x22,...xn/22x_1^2, x_2^2, ... x_{n/2}^2 或是繼續分治下去的 xx 也會是正負配對的?(這樣才能繼續遞迴下去嘛)經過多次嘗試後,我們發現 nn 次單位根 ω1\omega_1ω2\omega_2、...、 ωn1\omega_{n-1} 恰好會滿足這樣的性質!小教室 4告訴我們 ωk=ωk+n/2ωk\omega^k = -\omega^{k+n/2} \Rightarrow \omega^kωk+n/2\omega^{k+n/2} 是正負配對的。

代回上面的式子:

P(ωj)=Pe(ω2j)+ωjPo(ω2j)P(\omega^j) = P_e(\omega^{2j})+\omega^jP_o(\omega^{2j}) P(ωj)=Pe(ω2j)ωjPo(ω2j)P(-\omega^j) = P_e(\omega^{2j})-\omega^jP_o(\omega^{2j}) j{0,1,...,(n/21)}j \in \{0, 1, ..., (n/2-1)\}

Pe(x)P_e(x)Po(x)P_o(x) 利用 {ω0,ω2,...,ωn2}\{\omega^0, \omega^2, ..., \omega^{n-2}\} 繼續分治下去。最後答案的合併就很簡單了。

以下是 Cooley-Tukey Algorithm 的虛擬碼:

P=[P0,P1,...,Pn1]P = [P_0, P_1, ..., P_{n-1}]PP 是多項式的係數陣列)

y=[P(ω0),P(ω1),...,P(ωn1)]y = [P(\omega^0), P(\omega^1), ..., P(\omega^{n-1})]yy 是多項式的點值陣列)

Function FFT(PolynomialP):n=length of Pif n=1:Return Pω=e2πi/nPe=[p0,p2,...,pn2]Po=[p1,p3,...,pn1]ye=FFT(Pe)yo=FFT(Po)y=[0,0,...,0]  //There are n zeros.For j{0,1,...,n2}:y[j]=ye[j]+ωjyo[j]y[j+n/2]=ye[j]ωjyo[j]Return y\text{Function}\ FFT(\text{Polynomial} P): \\ \qquad n = \text{length of} \ P \\ \qquad \text{if}\ n = 1: \\ \qquad \qquad \text{Return} \ P \\ \qquad \omega = e^{2 \pi i / n} \\ \qquad P_e = [p_0, p_2, ..., p_{n-2}] \\ \qquad P_o = [p_1, p_3, ..., p_{n-1}] \\ \qquad y_e = FFT(P_e) \\ \qquad y_o = FFT(P_o) \\ \qquad y = [0, 0, ..., 0]\ \ // \text{There are }n\text{ zeros}. \\ \qquad \text{For}\ j \in \{0, 1, ..., n-2\}: \\ \qquad \qquad y[j] = y_e[j] + \omega^j y_o[j] \\ \qquad \qquad y[j+n/2] = y_e[j] - \omega^j y_o[j] \\ \qquad \text{Return}\ y

至於 FFT 的逆變換 (IFFT, Inverse FFT),則只要將虛擬碼中的 ω\omega 換成 1ne2πi/n\frac{1}{n} e^{-2 \pi i / n} 即可,詳細原因和范德蒙反矩陣有關,在此不贅述。

NTT (Number Theoretic Transform)

注意到此題需要將答案模 MM ,而當 NN 超過 350350 時便會超過 long long int 的範圍,且進行 FFT 時由於 xx 選用複數域的數因此無法邊做邊模。此時我們思考,有沒有什麼樣的東西,使得在 ZM\mathbb{Z}_M^* 域下還能有單位根那樣漂亮的性質?

MM 為質數,則由費馬小定理可知,對於任意正整數 aa,有

aM11(mod M)a^{M-1} \equiv 1 (mod\ M)

若存在原根 gg 使得

0i<j<M1, gigj(modM)\forall 0 \leq i < j < M-1,\ g^i \neq g^j \pmod{M}

則此時原本的 ω\omega 便可替換成 gM1Ng^\frac{M-1}{N},如此便可在 ZM\mathbb{Z}_M^* 域下做 FFT 了,此步驟稱為 NTT。而 NTT 的反運算則只要將 gM1Ng^\frac{M-1}{N} 替換成它的模逆元就行了。

由於 MM 為質數,因此對於任意一個正整數 aa,其模逆元即為 aM2a^{M-2}。證明也不難,只要將費馬小定理等式兩邊同乘 a1a^{-1} 就可以得到該結果。

實作上可以先預處理 gg 的冪次和模逆元,這樣能降低執行速度。

原根的定義請參考:http://gotonsb-numbertheory.blogspot.com/2014/06/primitive-roots.html

NTT 與常用多項式操作模板

2026/02/15 更新:放上模板與常用模數表。

分成兩個檔案,第一個檔案支援兩個多項式相乘,並支援 CRT 合併兩組或多組模數(以防中途係數相乘時溢位)。

第二個檔案支援多項式 AmodBA \bmod B,以及高速線性遞迴(Kitamasa Method)實作方式。

NTT 常用模數表

附上常用質數和其原根對照表:

P = r*2^k + 1
P                   r   k   g
998244353           119 23  3
1004535809          479 21  3
 
P                   r   k   g
3                   1   1   2
5                   1   2   2
17                  1   4   3
97                  3   5   5
193                 3   6   5
257                 1   8   3
7681                15  9   17
12289               3   12  11
40961               5   13  3
65537               1   16  3
786433              3   18  10
5767169             11  19  3
7340033             7   20  3
23068673            11  21  3
104857601           25  22  3
167772161           5   25  3
469762049           7   26  3
1004535809          479 21  3
2013265921          15  27  31
2281701377          17  27  3
3221225473          3   30  5
75161927681         35  31  3
77309411329         9   33  7
206158430209        3   36  22
2061584302081       15  37  7
2748779069441       5   39  3
6597069766657       3   41  5
39582418599937      9   42  5
79164837199873      9   43  5
263882790666241     15  44  7
1231453023109121    35  45  3
1337006139375617    19  46  3
3799912185593857    27  47  5
4222124650659841    15  48  19
7881299347898369    7   50  6
31525197391593473   7   52  3
180143985094819841  5   55  6
1945555039024054273 27  56  5
4179340454199820289 29  57  3
9097271247288401921 505 54  6