【多項式】FFT

【多項式】FFT

Preface

本文對所有 \(\LaTeX\) 編譯後生成的文本共有大約 \(7000\) 字,其中前半部分爲前置知識部分,介紹了多項式的有關概念、運算法則以及複數的概念、運算法則以及單位根有關內容,並證明了蝴蝶操作所用到的有關複數的兩個重要引理公式。如果你對上述內容已經有了解,可以跳過 Pre-knowledge 部分。Pre-knowledge 部分大約有 \(2000\) 字。

由於內容比較長,本文還沒有經審閱人完全審閱完成,如果您發現了文本中的錯誤,請私信或評論我指出。

Pre-knowledge

多項式

Definition

稱一個關於 \(x\) 的式子

\[f(x) = \sum_{i = 0}^{n} a_i \times x^i\]

爲一個 \(n\) 次多項式,其中 \(a_i\) 爲常數。稱 \(n\)\(f(x)\) 的次數。顯然,\(f(x)\) 可以看做一個關於 \(x\)\(n\) 次函數 \(y = f(x)\)

回憶初中解析幾何最後一個大題的第一問,正常情況下都是給定三個點的座標,求一個關於 \(x\) 的二次曲線解析式方程。而類似的如果求一條直線的解析式,則需要給出兩個點的座標。

類似的,對於如果想要確定一個 \(n\) 次函數的解析式,則需要 \(n + 1\) 個點的座標。這是因爲一個 \(n\) 次函數共有 \((n + 1)\) 個係數,根據某我忘了名字的基本定理,\(n\) 元非無解方程組有唯一解的必要條件是有 \(n\) 個本質不同的方程,因此這裏需要 \((n + 1)\) 個點構造出 \((n + 1)\) 個方程,才能求出這 \((n + 1)\) 個係數。

也就是說,只要給定了 \((n + 1)\) 個點的座標,就可以唯一確定一個 \(n\) 次函數的解析式,進而也就可以確定這個多項式。

那麼,給出 \((n + 1)\) 個互不相同的點的座標來確定一個多項式的形式,稱爲多項式的點值表示法。對應的,寫成 \(f(x) = \sum_{i = 0}^{n} a_i \times x^i\) 形式的多項式被稱爲多項式的係數表示法

Operation

在進行多項式運算時,如果兩個多項式的次數不一樣,注意到關於多項式次數的定義中沒有規定最高次項的系不能爲 \(0\),因此可以認爲次數較低的多項式的次數也爲較高的次數多項式的次數。

如果使用點值表示法進行多項式運算,則必須保證兩個多項式所給點值的橫座標一一對應相等。

以下設 \(n\) 次多項式 \(A(x) = \sum_{i = 0}^n a_i \times x^i\)\(B(x) = \sum_{i = 0}^{n} b_i \times x^i\)

多項式加減法:

兩個 \(n\) 次多項式 \(A(x)\)\(B(x)\) 相加減的結果爲

\[f(x)~ = ~A(x) \pm B(x)~ = ~\sum_{i = 0}^{n} (a_i \pm b_i) x^i\]

多項式乘法:

兩個 \(n\) 次多項式 \(A(x)\)\(B(x)\) 相乘的結果是一個 \(2n\) 次多項式

\[f(x)~=~A(x) \times B(x)~=~\sum_{i = 0}^n \sum_{j = 0}^n a_i \times b_j \times x^{i + j}\]

同時,上式顯然可以寫成這種形式

\[f(x) = \sum_{i = 0}^{2n} \sum_{j = 0}^{\min(i, n)} a_j \times b_{i - j} \times x^i\]

對於點值表示法,由於 \(A(x) \pm B(x)\)\(A(x) \times B(x)\) 本身表示的就是兩個多項式的值做加減乘法,因此直接將對應橫座標位置的縱座標相加/減/乘即可。需要特別注意的是,做乘法時,需要 \(A(x)\)\(B(x)\) 各給出 \(2n\) 組點值,而不是 \(n\) 組。

複數

Definition

定義常數 \(i\),滿足

\[i^2 = -1\]

則所有形如

\[z = a + b \times i,~~~a, b \in R\]

的數字 \(z\) 構成的集合稱爲複數集,記爲 \(C\)\(C\) 中的每個元素都稱作複數。

對於 \(z = a + bi\),稱 \(a\)\(z\) 的實部,\(b\)\(z\) 的虛部

Geometric Interpretation

複平面是一個笛卡爾平面,有兩條座標軸,縱軸爲虛軸,橫軸爲實軸,兩軸相互垂直。

對於一個複數 \(z = a + bi\),它在複平面上對應一個從原點指向 \((a,~b)\) 的向量。也即實軸座標爲實部值,虛軸座標爲虛部值的點。

顯然複平面上從原點出發的任意一個向量也對應唯一的一個複數,也即向量 \((a, b)\) 對應一個複數 \(z = a + b_i\)

易證複數與複平面上從原點出發的向量是一一對應關係。

以實軸正方向爲始邊,\(z\) 所對應的向量 \(Z\) 爲終邊的角 \(\theta\) 稱爲複數 \(z\) 的幅角。

Operation

複數的模:

複數 \(z= a + b_i\) 的模爲其在複平面上對應向量的長度,記做\(|z|\)

\[|z| = \sqrt{a^2 + b^2}\]

共軛複數:

複數 \(z\) 在複平面上對應的向量關於實軸對稱後對應的複數稱爲 \(z\) 的共軛複數,記做 \(\overline{z}\)

\(z\)\(\overline z\) 滿足如下關係:

\(z\) 的幅角爲 \(\theta_0\)\(\overline z\) 的幅角爲 \(\theta_1\)

\[\theta_0 + \theta_1 = \pi\]

\[|z| = |\overline z|\]

且兩者的實部相同,虛部互爲相反數。

複數加減法:

兩個複數 \(z_1 = a_1 + b_1i,~z_2 = a_2 + b_2 i\) 相加減的結果爲

\[z_0~=~z_1 \pm z_2~=~(a_1 \pm a_2) + (b_1 \pm b_2)i\]

在複平面上,兩個複數相加減的結果爲他們所對應的向量按照平行四邊形定則相加減

複數乘法:

兩個複數 \(z_1 = a_1 + b_1i,~z_2 = a_2 + b_2 i\) 相乘的結果爲

\[z_0~=~z_1 \times z_2~=~(a_1 + b_1i) \times (a_2 + b_2i) = a_1a_2 + a_1b_2i + a_2b_1i + b_1b_2i^2\]

又因爲

\[i^2 = -1\]

所以

\[z_0~=~(a_1a_2 - b_1b_2) + (a_1b_2 + a_2b_1) i\]

在複平面上,\(z_1,~z_2,~z_0\) 所對應的幅角 \(\theta_1,~\theta_2,~\theta_0\) 有如下關係:

\[\theta_0 = \theta_1 + \theta_2\]

他們的模有如下關係

\[|z_0| = |z_1| \times |z_2|\]

考慮 \(z = a + b_i\) 和它的共軛複數 \(\overline z = a - b_i\)

\[z \times \overline z~=~(a + bi) \times (a - bi) = a^2 + b^2\]

因此兩個互爲共軛複數的數之積一定是一個實數。

複數除法:

對於兩個複數 \(z_1 = a_1 + b_1i,~z_2 = a_2 + b_2 i\),他們相除的結果爲

\[z_0 = \frac{z_1}{z_2}\]

考慮分數上下同時乘 \(\overline{z_2}\),有

\[z_0~=~\frac{z_1 \overline {z_2}}{a_2^2 + b_2^2}\]

分母是一個實數,可以直接將分子的實部虛部除以分母。

複數指數冪:

有歐拉公式

\[e^{i\theta} = \cos \theta + i \sin \theta\]

其中 \(e\) 是自然對數的底數

當取 \(\theta = \pi\) 時,有

\(e^{i\pi} = \cos \pi + i \sin \pi\)

又因爲 \(\cos \pi = 1,~sin \pi = 0\)

所以 \(e^{i\pi} = 1\)

單位根

Definition

在複數域下,滿足 \(x^n = 1\)\(x\) 被稱爲 \(n\) 次單位根

根據代數基本定理,\(n\) 次單位根一共有 \(n\) 個。

經過計算可得,將所有的 \(n\) 次單位根按照幅角大小排列,第 \(k\) \((0 \leq k < n)\)\(n\) 次單位根爲

\[x_k~=~e^{i \frac{2 k\pi}{n}}\]

Proof

\[x_k^n~=~e^{i 2 k\pi}\]

根據歐拉公式

\[e^{i2k\pi} = \cos 2k\pi + i \sin 2k\pi\]

又因爲

\[\cos 2k\pi = 1,~~\sin 2k\pi~=~0\]

所以

\[x_k^n = e^{i\frac{2k\pi}{n}}\]

是原方程的一個解。

顯然這 \(n\) 個解是互不相同的,又根據代數基本定理,該方程有且僅有 \(n\) 個解。因此該方程解的與 \(x_k\) 一一對應。證畢。

Property

因爲 \(\sin^2 \theta + \cos^2 \theta~=~1\),所以所有的 \(n\) 次單位根的模都是 \(1\)

\(n\) 個單位根在複平面上平分單位圓。他們與單位圓的 \(n\) 等分線所在線段分別重合。

本原單位根

Definition

\(0\)\((n - 1)\) 次方的值能生成所有 \(n\) 次單位根的 \(n\) 次單位根稱爲爲 \(n\) 次本原單位根。

顯然 \(x_1 = e^{i \frac{2\pi}{n}}\) 是一個本原單位根。

證明上可以考慮利用複平面上單位根等分單位圓,且兩複數相乘時幅角相加模相乘來證明,這裏略去。

\(n\) 次本原單位根爲 \(\omega_n = e^{i \frac{2\pi}{n}} = \cos \frac{2\pi}{n} + i \sin \frac{2\pi}{n}\)

\(n\) 次本原單位根可能不止一個,但是下文的“本原單位根”特指 \(e^{i \frac{2\pi}{n}}\)

Property

\(n\) 是一個正偶數,且 \(n = 2m\),有:

\[(\omega_{n}^k)^2~=~\omega_m^k\]

\[\omega_n^{m + k}~=~-\omega_{n}^k\]

Proof

一式:

考慮兩個 \(\omega_n^k\) 相乘,幅角相加,爲之前幅角的兩倍,而模爲 \(1 \times 1 = 1\)

所以相乘的結果爲一個幅角爲 \(\omega_n^k\) 幅角兩倍的單位向量。容易驗證 \(\omega_m^k\) 的幅角是 \(\omega_n^k\) 幅角的兩倍,且模爲 \(1\)。由於向量和複數是一一對應的,所以一式成立。

二式:

顯然 \(\omega_n^m\) 的幅角爲 \(\pi\)。等式左邊可以寫成 \(w_{n}^k \times w_n^m\),即爲 \(w_n^k\) 繞原點旋轉 \(\pi\) 弧度。根據平面解析幾何定理,旋轉前後的兩個向量橫縱座標分別互爲相反數。證畢。

Pre-knowledge部分結束了。


Algorithm

考慮對兩個多項式做乘法,如果運用係數表示法,顯然需要 \(O(n^2)\) 的時間複雜度,而如果已知兩個多項式的點值表示法,則只需要 \(O(n)\) 的時間複雜度。因爲只需要將對應的點值縱座標相乘就可以了。

但是我們將一個多項式從係數表示法改爲點值表示法(稱爲求值)需要 \(O(n^2)\) 的複雜度(因爲每個橫座標都需要 \(O(n)\) 的時間去計算),而將一個點值表示法改爲係數表示法(稱爲插值)則需要 \(O(n^3)\) 的複雜度來做高斯消元。但是隻要我們將這兩步都優化至低於 \(O(n^2)\) 的複雜度,就可以得到一個比直接用係數表示法乘更優的做法。

而求出一個 \(n\) 次多項式在每個 \(n\) 次單位根下的點值的過程,被稱爲離散傅里葉變換(Discrete Fourier Transform,DFT),而將這些點值重新插值成係數表示法的過程,叫做離散傅里葉逆變換(Inverse Discrete Fourier Transform,IDFT)

以下設進行變換的多項式爲 \((n - 1)\) 次多項式 \(A(x) = \sum_{i = 0}^{n - 1} a_i \times x^i\)。其中 \(n\)\(2\) 的整數冪。如果不是的話,則向 \(A(x)\) 的更高次數位 \(n\) 補充 \(a_n = 0\) ,令其成爲 \(n\) 次多項式,一直進行直到其次數+1的值是 \(2\) 的整數冪,取 \(n\) 等於其次數,\(m = \frac{n}{2}\)

DFT

考慮求出一個長度爲 \(n\) 數列 \(\{b_i\}\),這個數列的第 \(k\) 項爲 \(A(x)\)\(n\) 次單位根的 \(k\) 次冪處的點值。

因此有

\[b_k~=~\sum_{i = 0}^{n - 1} a_i \times \omega_{n}^i\]

注意上式中的 \(i\)\(\Sigma\) 循環的循環變量,而不是 \(-1\) 的二次方根。

這個過程是 \(O(n^2)\) 的,我們考慮使用快速傅里葉變換(Fast Fourier Transform,FFT)來優化這個過程。

FFT

我們考慮對 \(A(x)\) 按照係數角標的奇偶性分類,即

\[A(x)~=~\sum_{i = 0}^{n - 1} a_i x^i~=~\sum_{i = 0}^m a_{2i} \times x^{2i} + \sum_{i = 0}^m a_{2i + 1} \times x^{2^i + 1}\]

對於上式的後半部分,提出一個 \(x\),得到

\[A(x)~=~\sum_{i = 0}^{m - 1} a_{2i} x^{i2} + x\sum_{i = 0}^{m - 1} a_{2i + 1} x^{2i}~=~\sum_{i = 0}^{m - 1} a_{2i} (x^{2})^{i} + x\sum_{i = 0}^{m - 1} a_{2i + 1} (x^2)^{i}\]

\(A_0(x)\) 是一個 \((m - 1)\) 次多項式,滿足

\[A_0~=~\sum_{i = 0}^{m - 1} a_{2i} x^i\]

\(A_1(x)\) 是一個 \((m - 1)\) 次多項式,滿足

\[A_1~=~\sum_{i = 0}^{m - 1} a_{2i + 1}x^i\]

聯立以上三式,可以得到

\[A(x)~=~A_0(x^2) + x \times A_1(x^2)\]

如果求出了 \(A_0\)\(A_1\) 在各點的點值,由於上式可以 \(O(1)\) 計算,所以我們可以 \(O(n)\) 計算 \(A(x)\) 在各個點的點值了。而求 \(A_0\)\(A_1\) 的過程和求 \(A\) 的過程完全一致,因此可以遞歸處理。

但是很遺憾,我們 \(A_0\)\(A_1\) 各需要遞歸一次,根據主定理,這樣的時間複雜度是 \(O(n^2)\) 的。

但是考慮我們求的是在 \(n\) 次單位根的各個冪次下的點值,根據 Pre-Knowledge 的最後一部分,我們得到了公式

\[(\omega_{n}^k)^2~=~\omega_m^k\]

\[\omega_n^{m + k}~=~-\omega_{n}^k\]

第二個式子使得我們考慮小於 \(m\) 次的點值和大於 \(m\) 次點值之間的關係。

對於 \(0 \leq k < m\),我們有

\[A(\omega_n^k)~=~A_0((\omega_n^k)^2) + w_n^kA_1((\omega_n^k)^2)\]

根據上面的第一個公式,化簡得到

\[A(\omega_n^k)~=~A_0(\omega_m^k) + w_n^kA_1(\omega_m^k)\]

這只是前半部分的次數,我們考慮後半部分次數:

\[A(\omega_n^{m + k})~=~A_0((\omega_n^{m + k})^2) + \omega_n^{m + k} A_1((\omega_n^{m + k})^2)\]

根據第二個公式,化簡得到

\[A(\omega_n^{m + k})~=~A_0((w_n^k)^2)+-\omega_n^k A_1((\omega_n^k)^2)\]

再應用第一個公式得到

\[A(\omega_n^{m + k})~=~A_0(\omega_m^k) - w_n^kA_1(\omega_m^k)\]

我們驚喜的發現,大於 \(m\) 次的點值可以由 \(A_0\)\(A_1\) 在小於 \(m\) 次的點值求出。只要求出了 \(A_0\)\(A_1\) 在小於 \(m\) 次的點值,就可以線性求出 \(A\) 在整個 \(n\) 次冪處的點值。而求 \(A_0\)\(A_1\) 在小於 \(m\) 次的點值也是可以遞歸求解的。

考慮時間複雜度:遞推關係爲 \(T(n)~=~2T(n / 2) + O(n)\)。因爲 \(O(n)~=~\Theta(n)\),所以 \(T(n)~=~\Theta(n^{\log_2^2} \log n) = \Theta(n \log n)\)

由此,得到了快速計算 DFT 的辦法,並證明了其時間複雜度爲 \(O(n \log n)\)。這種方法被即爲 FFT

上面推導出 \(A(\omega_n^k)\)\(A(\omega_n^{m + k})\) 的值的式子被稱爲蝴蝶操作(Butterfly Operation)。這個名字的由來是如果將 \(A\) 在各處的點值畫成一個長條形的數組,在數組下面一次依次是 \(n\) 次本原單位根的 \(0 \sim m - 1\) 次冪,則求值過程可以畫成 \(\omega_n^0\) 連一條邊向 \(A(\omega_n^0),~A(\omega_n^{\frac{n}{2} - 1})\)\(\omega_n^1\) 連向 \(A(\omega_n^1),~A(\omega_n^\frac{n}{2})\),以此類推。畫出的圖形如同蝴蝶的翅膀。

IDFT

至此,我們已經有了 \(O(n \log n)\) 的算法來計算兩個係數表示法的多項式相乘後的點值表示。接下來我們只需要用 \(O(n \log n)\) 的時間複雜度完成插值的過程,就可以得到一個完整的 \(O(n \log n)\) 的係數型多項式乘法算法了。

我們目前已知一個 \((n - 1)\) 次多項式 \(A(x)~=~\sum_{i = 0}^{n - 1} a_i x^i\) 進行了離散傅里葉變換後的點值 \(\{b_i\}\),即

\[b_k~=~\sum_{i = 0}^{n - 1} a_i \times \omega_n^{ik}\]

現在試圖還原係數數列 \(\{a_i\}\)

推導過程比較複雜,我們直接給出結論:

\[a_k~=~\frac{1}{n} \sum_{i = 0}^{n - 1} b_i \omega_n^{-ki}\]

下面證明上面這個式子是 DFT 式子(即上面計算 \(b_k\) 的式子)的逆變換。

我們首先將 DFT 的式子帶入 \(\sum_{i = 0}^{n - 1} b_i \omega_n^{-ki}\)

得到上式爲

\[\begin{align} \sum_{i = 0}^{n - 1} b_i \omega_n^{-ki}~ & =~\sum_{i = 0}^{n - 1} \sum_{j = 0}^{n - 1} a_j \times \omega_n^{ji} \times \omega_n^{-ki}\\ & =~\sum_{i = 0}^{n - 1}\sum_{j = 0}^{n - 1} a_j \times \omega_n^{ij - ki}\\ & =~\sum_{j = 0}^{n - 1} a_j \times \sum_{i = 0}^{n - 1} \omega_n^{i(j - k)} \end{align}\]

考慮分類討論。

1、當 \(j = k\) 時:

\(j - k = 0\),因此 \(\omega_{n}^{i(j - k)} = \omega_n^0 = 1\)。於是

\[\sum_{i = 0}^{n - 1} \omega_n^{i(j - k)}~=~n \times 1 = n\]

2、當 \(j \neq k\) 時:

顯然 \(|j - k| < n\),因此 \(\omega_n^{j - k} \neq 1\),則 \(\sum_{i = 0}^{n - 1} \omega_n^{(j - k) i}\) 是一個公比不爲 \(1\) 的等比數列前綴和。根據等比數列求和公式得到

\[\sum_{i = 0}^{n - 1} \omega_n^{(j - k) i}~=~\frac{1 - \omega_n^{(n - 1)(j - k)} \times \omega_n^{j - k}}{1 - \omega_{n}^{j - k}}~=~\frac{1 - \omega_n^{n(j - k)}}{1 - \omega_{n}^{j - k}}~=~\frac{1 - (\omega_n^{(j - k)})^n}{1 - \omega_{n}^{j - k}}\]

根據複數的幾何性質,易證對於所有的 \(n\) 次單位根 \(T\)\(T^{x + n}~=~T^{x}\),其中 \(x\) 爲任意整數。

\(\sum_{i = 0}^{n - 1} \omega_n^{(j - k) i}~=~\frac{1 - (\omega_n^{(j - k)})^0}{1 - \omega_{n}^{j - k}}~=~\frac{1 - 1}{1 - \omega_n^{j - k}}~=~0\)

第二種情況的證明被稱爲消去引理

綜上討論,當 \(j \neq k\) 時,因爲另一個因數是 \(0\),前面的 \(\sum a_j\) 不會對式子產生貢獻,而 \(j = k\) 時,會對答案產生 \(n\) 倍的貢獻。

原式可以寫成

\[\sum_{i = 0}^{n - 1} b_i \omega_n^{-ki}~=~~\sum_{j = 0}^{n - 1} a_j \times \sum_{i = 0}^{n - 1} \omega_n^{i(j - k)}~=~\sum_{j = 0}^{n - 1} a_j \times [j = k] \times n~=~a_k \times n\]

將上式帶入 IDFT 的式子 \(a_k~=~\frac{1}{n} \sum_{i = 0}^{n -1} b_i \omega_n^{-ki}\) 的右邊,得到

\[\text{右邊}~=~\frac{1}{n} a_k \times n~=~a_k~=~\text{左邊}\]

類似的,可以證明將 IDFT 的式子帶入 DFT 也是可以使等式成立的,這裏略去。

由此,我們證明了變換

\[a_k~=~\frac{1}{n} \sum_{i = 0}^{n - 1} b_i \omega_n^{-ki}\]

DFT 的逆變換,稱爲 IDFT。我們可以通過這個式子求出這個多項式的係數表示法。

IFFT

下面的問題是如何用較低的複雜度計算 \(B(x)~=~\sum_{i = 0}^{n - 1} b_i \times x^i\)\(w_n^{-ki}\),其中 \(0 \leq k < n\) 處的點值。

\(w_n^{-k}\) 可以看做 \(n\) 次本原單位根每次逆時針旋轉本原單位根幅角的弧度,因此 \(\omega_n^{-k}\)\(\omega_n^k\) 是一一對應的。具體的,\(w_n^{-k} = w_n^{k + n}\)。因此我們只需要使用 FFT 的方法,求出 \(B(x)\)\(\omega_n\) 各個冪次下的值,然後數組反過來,即令 \(a_k~=~\frac{1}{n} \sum_{i = 0}^n B(w_n^{n - k})\) 即可。

這一步快速計算插值的過程叫做快速傅里葉逆變換(Inverse Fast Fourier Transform,IFFT)

至此,我們得到了一個時間複雜度爲 \(O(n \log n)\) 的多項式乘法計算方法。

Code

根據上面的推導,我們可以很輕鬆的寫出 FFT 的遞歸形式:

void FFT(std::complex<double> *A, int N) {
  if (N == 1) {
    return;
  }
  int M = N >> 1;
  std::complex<double> A0[M], A1[M];
  for (int i = 0; i < M; ++i) {
    A0[i] = A[i << 1];
    A1[i] = A[(i << 1) | 1];
  }
  FFT(A0, M); FFT(A1, M);
  auto W = std::complex<double>(cos(1.0 * PI / M), sin(1.0 * PI / M)), w = std::complex<double>(1.0, 0.0);
  for (int i = 0; i < M; ++i) {
    A[i] = A0[i] + w * A1[i];
    A[i + M] = A0[i] - w * A1[i];
    w *= W;
  }
}

以及 IFFT

void IFFT(std::complex<double> *A, int N) {
  FFT(A, n)
  std::reverse(A + 1, A + N);
}

需要注意的是,因爲 \(\omega_n^0~=~\omega_n^n\),所以是第 \(1\) 個點值和第 \(N - 1\) 個交換,而不是第 \(0\) 個點值和第 \(N - 1\) 個交換。

optimization

上面這個 FFT 交到 luogu 上以後只有 \(77\) 分。他的複雜度顯然是 \(O(n \log n)\) 的,但是遞歸和動態開空間帶來的巨大常數讓他難以通過 \(10^6\) 的數據。

我們考慮優化上面的代碼。

首先我們注意到我們動態申請和刪除了 \(O(n \log n)\) 的空間,但是我們同一時刻只最多需要 \(2N\) 的空間,而遞歸調用又是棧式的,即先申請的後刪除。這意味着如果用一個數組做內存池,沒有被分配的內存總是連續的,被分配的內存也總是連續的。並且分配一個數組可以 \(O(1)\) 而不是 \(O(size)\) 完成。

然而這個優化並沒有什麼卵用。

我們考慮將上面的遞歸改成迭代。

既然遞歸是自上而下的,那麼我們的迭代就是一個自下而上的合併過程。當 \(n = 8\) 時,我們考慮遞歸時調用原多項式係數下標的遞歸樹:

step 1: 0 1 2 3 4 5 6 7
step 2: 0 2 4 6,  1 3 5 7
step 3: 0 4,  2 6,  1 3,  5 7
step 4: 0,  4,  2,  6,  1,  3,  5,  7

上表的 step 代表了遞歸的層數,冒號後的數字代表數字的下標。我們考察最後算的一層,也就是第 \(4\) 層的二進制值:

000, 100, 010, 110, 001, 011, 101, 111

看起來還是沒有頭緒,但是我們將二進制值反過來

000, 001, 010, 011, 100, 110, 101, 111

我們發現上面 \(8\) 個二進制的排列是單調遞增的,在十進制下分別是

0, 1, 2, 3, 4, 5, 6, 7

因此我們只需要將數列 \(\{a\}\) 按照下標二進制翻轉後的大小排序,得到序列 \(\{a'\}\)。遞歸倒數第二層對於 \(\{a\}\) 的蝴蝶操作就變成了對於 \(\{a'\}\) 相鄰兩個數進行合併。而上面幾層同理。

因此問題變成了如何在 \(O(n \log n)\) 的時間內將序列按照下標二進制翻轉後排序得到的序列。

顯然對於任何一個數我們都可以 \(O(\log n)\) 的運用進制轉換來確定其二進制逆序值,但是這樣因爲涉及到了大量的除法和取模,常數很大,我們考慮規避這個做法。

我們考慮基數排序的操作,我們可以將一個數的數位分爲兩半,先對後半部分數位構成的數進行排序,然後再對前半部分數位構成的數進行排序。兩個數字的前半部分數位如果相同,那麼它們的先後順序即爲後半部分數位的先後順序。這樣由於後半部分本身是有序的,就可以自低位向高位對數列進行排序。

同樣的,我們自低位向高位對每個數的二進制逆序排序。

初始時,序列裏只有兩個數

000, 100

他們逆序後爲

000, 001

分別是最小的和次小的翻轉值。

然後我們考慮將這兩個數的第 \(2\) 位(最右側爲最低位,最低位爲第 \(1\) 位)都置爲 \(1\),得到了兩個數

010, 110

將這兩個數也加入序列中,得到

000, 100, 010, 110

翻轉值即爲

000, 001, 010, 011

注意到第 \(i\) 次操作的時候,加入了大於 \(i\) 位的位置都是 \(0\),且第 \(i\) 位是 \(1\) 的所有數,他們顯然比序列中原來存在的大於等於 \(i\) 爲的位置都是 \(0\) 的數要大,而比大於 \(i\) 位存在 \(1\) 的數小,並且去掉 \(1\) 以後即爲序列中原有數字的順序。這樣我們就證明了這個方法的正確性。

而我們只需要處理 \(O(\log n)\) 位,每位的處理都是 \(O(n)\) 的,因此總時間複雜度 \(O(n \log n)\),空間複雜度 \(O(n)\),事實上對於每位的處理是跑不滿 \(O(n)\) 的,並且規避了取模和除法操作,常數極小。

代碼如下

void MakeRev(const int N) {
  int d = N >> 1, p = 0;
  tax[p++] = 0;
  tax[p++] = d;
  for (int w = 2; w <= N; w <<= 1) {
    d >>= 1;
    for (int p0 = 0; p0 < w; ++p0) {
      tax[p++] = tax[p0] | d;
    }
  }
}

然後考慮通過我們得到的 tax 數組來對原數組 \(A\) 進行排序。

\(rev(i)\)\(i\) 二進制逆序後的值,顯然 \(rev(rev(i)) = i\)。因此對於一個下標 \(p\),設它排序後在新序列中的下標爲 \(q\),那麼原序列下標爲 \(q\) 的位置在新序列的下標一定是 \(p\)。所以我們只需要對於所有 \(rev(i) > i\) 的位置 \(i\),交換 \(i\)\(rev(i)\) 位置的值即可。

這部分操作的名字叫做位逆序置換

for (int i = 1; i < N; ++i) if (tax[i] > i) {
  std::swap(A[i], A[tax[i]]);
}

最後考慮迭代的蝴蝶操作過程。

對於求\(A(x)\)\(n\) 次單位根的各冪次的點值時,\(m = \frac{n}{2}\) 次單位根的各冪次在 \(A_0\)\(A_1\) 處的點值已經被計算並存儲在了 \(A\) 數組中。我們令 \(A_0(\omega_m^k)~=~A[k]\)\(A_1(\omega_m^k)~=~A[k + m]\)。那麼 \(A(\omega_n^k)~=~A[k] + \omega_n^k \times A[k + m]\)\(A(\omega_n^{k + m})~=~A[k]~-\omega_n^k \times A[k + m]\),並將上面兩個值分別存入 \(A[k]\)\(A[k + m]\) 中即可。其中 \(A(x)\) 代表多項式,\(A[k]\) 代表數組。那麼在更上面一層遞歸調用時,根據 \(A_0\)\(A_1\)的定義,當前的 \(A[k]\) 即是上面一層所計算的多項式 \(A'(x)\) 的導出多項式 \(A'_0(x)\)\(\omega_n^k\) 處的點值,\(A[k + m]\) 即是 \(A'_1(x)\)\(\omega_n^k\) 處的點值。

至此,我們得到了一個迭代完成 FFT 的算法。

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <complex>
#include <iostream>
#include <algorithm>

const int maxn = 6000006;
const double PI = acos(-1);

int n, m, k, sm;
int tax[maxn];
std::complex<double> F[maxn], G[maxn];

void MakeRev(const int N);
void FFT(std::complex<double> *A, int N);

int main() {
  freopen("1.in", "r", stdin);
  qr(n); qr(m);
  for (int i = 0, x; i <= n; ++i) {
    x = 0; qr(x); F[i] = x;
  }
  for (int i = 0, x; i <= m; ++i) {
    x = 0; qr(x); G[i] = x;
  }
  sm = n + m; k = 1;
  while (k <= sm) k <<= 1;
  MakeRev(k);
  FFT(F, k); 
  FFT(G, k);
  for (int i = 0; i < k; ++i) {
    F[i] *= G[i];
  }
  FFT(F, k);
  std::reverse(F + 1, F + k);
  for (int i = 0; i < sm; ++i) {
    qw(int((F[i].real()) / k + 0.5), ' ', true);
  }
  qw(int(F[sm].real() / k + 0.5), '\n', true);
  return 0;
}

void FFT(std::complex<double> *A, int N) {
  for (int i = 1; i < N; ++i) if (tax[i] > i) {
    std::swap(A[i], A[tax[i]]);
  }
  for (int len = 2, M = 1; len <= N; M = len, len <<= 1) {
    std::complex<double> W(cos(PI / M), sin(PI / M)), w(1.0, 0.0);
    for (auto L = 0, R = len - 1; R <= N; L += len, R += len) {
      auto w0 = w;
      for (auto p = L, lim = L + M; p < lim; ++p) {
        auto x = A[p] + w0 * A[p + M], y = A[p] - w0 * A[p + M];
        A[p] = x; A[p + M] = y;
        w0 *= W;
      }
    }
  }
}

void MakeRev(const int N) {
  int d = N >> 1, p = 0;
  tax[p++] = 0;
  tax[p++] = d;
  for (int w = 2; w <= N; w <<= 1) {
    d >>= 1;
    for (int p0 = 0; p0 < w; ++p0) {
      tax[p++] = tax[p0] | d;
    }
  }
}

Appreciation

十分感謝 @Dusker 抽出時間審閱這篇長文。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章