https://hackmd.io/@peter12345678/BkZ8wIFvO --- # FFT --- ## 多項式乘法 ---- ### 表示方法 ---- ##### 係數表示法$$A(x) = \displaystyle\sum_{i=0}^na_i\,x^i = a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}\,x^{n-1} + a_n\,x^n$$ ---- ##### 點值表示法 * 對於一個 $n$ 次多項式,我們可以隨機選擇 $n+1$ 個**不同的** $x_i$ ,並且對 $A$ 取值。 * 這種表示方法雖然不唯一,但**任何一種點值表示**都能藉由**插值法唯一**確定一個多項式。 $\{(x_i, A(x_i)) \mid 0 \le i \le n, i \in \mathbb Z \}$ --- #### 乘法 給定兩個多項式 $A(x), B(x)$ ##### $A(x) = \displaystyle\sum_{i=0}^na_i\,x^i = a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}\,x^{n-1} + a_n\,x^n$ ##### $B(x) = \displaystyle\sum_{i=0}^nb_i\,x^i = b_0 + b_1x + b_2x^2 + \cdots + b_{n-1}\,x^{n-1} + b_n\,x^n$ ---- ##### 將$A(x), B(x)$這二個多項式相乘得到 $C(x) = \displaystyle\sum_{i=0}^{2n}c_i\,x^i$,且 $c_i = \displaystyle\sum_{j=0}^ia_j\,b_{i-j}\,x^i$ ---- 在係數表示下做乘法,時間複雜度為 $O(n^2)$;但若改為點值表示,我們可以挑出 $2n + 1$ 個點並且直接相乘,時間複雜度為 $O(n)$。 ---- 如果能找出一種快速對多項式插值的方法,我們就能更有效率地計算多項式乘法。 但顯然的,你高中所認識牛頓、拉格朗日插值法,時間複雜度皆為 $O(n^2)$ --- ## 快速傅立葉變換 ## Fast Fourier Transform ---- ### 離散傅立葉變換 ### Discrete Fourier Transform ---- 取樣時選擇**夠好**的點,讓取樣插值都超快!!! ---- 假設現在有一個 $n - 1$ 次多項式 $A(x) = \displaystyle\sum_{i=0}^{n-1} a_ix^i$ 為求方便,令 $n = 2 ^k, k \in Z$,若高次項不足可補 $0$ ---- 重點來了!!! 我們選擇 $n$ 個 $n$ 次單位根 帶入 $A(x)$ 轉換成點值表示! ---- 什麼是單位根? 它是 $x^n = 1$ 的解,共有 $n$ 個複數解,分別為 $e^{2\pi ki/n}, k = 0,1, \dots, n-1$ ---- 令 $\omega_n = e^{2 \pi i/n}$,則 $n$ 個單位根分別為 $\omega_n^0, \omega_n^1, \dots, \omega_n^{n-1}$ 我們將代入的**點值向量** $\vec y = (A(\omega^0_n), A(\omega^1_n), \dots, A(\omega^{n-1}_n))$ 稱作**係數向量**的**離散傅立葉變換**,記作 $\vec y = \text{DFT}_n(\vec a)$ ---- 不過,這樣的變換還是需要 $O(n^2)$ 的時間複雜度 ---- #### Cooley-Tukey Algorithm ---- 我們將點值向量的每一項按照次數奇偶分類 $A(\omega^k_n) = \displaystyle\sum_{i=0}^{n-1}a_i\omega^{ki}_n$ $\hphantom{A(\omega^k_n) }= \displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i}\omega^{2ki}_n + \omega^k_n\displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i+1}\omega^{2ki}_n$ 配合單位根的性質 $\omega^2_n = \omega_{\frac n 2}$、 $\omega^{k + \frac n 2}_n = \omega^k_n \cdot \omega^{\frac n 2 }_n= -\omega^k_n$ 可發現 $k < \frac n 2$ 的時候 $A(\omega^k_n) = \displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i}\,\omega^{ki}_{n/2} + \omega^k_n\displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i+1}\,\omega^{ki}_{n/2}$ $\hphantom{A(\omega^k_n)} = A_1(\omega_{n/2}^k) + \omega^k_n\,A_2(\omega_{n/2}^k)$ $A(\omega^{k+\frac n 2}_n)= \displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i}\,\omega^{(k+\frac n 2)i}_{\frac n 2} + \omega^{k+\frac n 2}_n\displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i+1}\,\omega^{(k+\frac n 2)i}_{\frac n 2}$ $\hphantom{A(\omega^{k+\frac n 2}_n)}= \displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i}\,\omega^{ki}_{\frac n 2} - \omega^k_n\displaystyle\sum_{i=0}^{\frac n 2-1}a_{2i+1}\,\omega^{ki}_{\frac n 2}$ $\hphantom{A(\omega^{k+\frac n 2}_n)} = A_1(\omega_{\frac n 2}^k) - \omega^k_n\,A_2(\omega_{\frac n 2}^k)$ $\vec {a_1}$ 為 $(a_0, a_2, \dots, a_{n-2})$,$\vec {a_2}$ 為 $(a_1, a_3, \dots, a_{n-1})$ 我們可以藉由計算 $\vec {y_1} = \text{DFT}_{\frac n 2}(\vec {a_1})$、$\vec {y_2} = \text{DFT}_{\frac n 2}(\vec {a_2})$ $O(n)$ 合併出 $\vec y = \text{DFT}_n(\vec a)$ 的答案 問題就被分割成二個規模減半的子問題,時間複雜度為 $$T(n) = 2\,\,T{\small \left(\frac n 2\right)} + O(n) = O(n\log n)$$ 跟 merge sort 的複雜度一樣 #### 逆離散傅立葉變換 Inverse Discrete Fourier Transform 這個問題其實是相當於解一個線性方程組 $\begin{cases} \quad a_0\left(\omega_n^0\right)^0 &+ \quad a_1\left(\omega_n^0\right)^1 &+ \quad\cdots &+ \quad a_{n-1}\left(\omega_n^0\right)^{n-1} &= \quad A\left(\omega_n^0\right) \\ \quad a_0\left(\omega_n^1\right)^0 &+ \quad a_1\left(\omega_n^1\right)^1 &+ \quad\cdots &+ \quad a_{n-1}\left(\omega_n^1\right)^{n-1} &= \quad A\left(\omega_n^1\right) \\ &\quad\vdots \\ \quad a_0\left(\omega_n^{n-1}\right)^0 &+ \quad a_1\left(\omega_n^{n-1}\right)^1 &+ \quad\cdots &+ \quad a_{n-1}\left(\omega_n^{n-1}\right)^{n-1} &= \quad A\left(\omega_n^{n-1}\right) \\ \end{cases}$ 寫成矩陣形式的話 $\begin{bmatrix} \left(\omega_n^0\right)^0 & \left(\omega_n^0\right)^1 & \cdots & \left(\omega_n^0\right)^{n-1} \\ \left(\omega_n^1\right)^0 & \left(\omega_n^1\right)^1 & \cdots & \left(\omega_n^1\right)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ \left(\omega_n^{n-1}\right)^0 & \left(\omega_n^{n-1}\right)^1 & \cdots & \left(\omega_n^{n-1}\right)^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} A\left(\omega_n^0\right) \\ A\left(\omega_n^1\right) \\ \vdots \\ A\left(\omega_n^{n-1}\right) \end{bmatrix}$ 記上面的係數矩陣為 $V,\ v_{ij} = \omega^{ij}_n$ 構造矩陣 $D, \ d_{ij} = \omega^{-ij}_n$ 令他們相乘的結果為 $E = D \cdot V$ $$e_{ij} = \displaystyle\sum_{k=0}^{n-1} d_{ik}\,v_{kj} \\ \ \ \ \ \ \ \ \ = \displaystyle\sum_{k=0}^{n-1} \omega_n^{-ik}\,\omega_n^{kj} \\ \ \ \ \ \ = \displaystyle\sum_{k=0}^{n-1} \omega_n^{k(j-i)} \\ $$ 當 $i = j$ ,$e_{ij} = n$ 當 $i \ne j$ ,$e_{ij} = {\Large\frac {1 - (\omega_n^{j-i})^n}{1 - \omega_n^{j-i}}} = 0$ 可得到 $I_n = \frac 1 n E, \ \frac 1 n D = V^{-1}$ 所以 $\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = {\Large\frac{1}{n}} \begin{bmatrix} \left(\omega_n^{-0}\right)^0 & \left(\omega_n^{-0}\right)^1 & \cdots & \left(\omega_n^{-0}\right)^{n-1} \\ \left(\omega_n^{-1}\right)^0 & \left(\omega_n^{-1}\right)^1 & \cdots & \left(\omega_n^{-1}\right)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ \left(\omega_n^{-(n-1)}\right)^0 & \left(\omega_n^{-(n-1)}\right)^1 & \cdots & \left(\omega_n^{-(n-1)}\right)^{n-1} \\ \end{bmatrix} \begin{bmatrix} A\left(\omega_n^0\right) \\ A\left(\omega_n^1\right) \\ \vdots \\ A\left(\omega_n^{n-1}\right) \end{bmatrix}$ IDFT 就相當於 DFT 過程中 $\omega_n^i$ 換成 $\omega_n^{-i}$,結果再除以 $n$ ### 實作 #### INIT 提前先將所有 $\omega_n^k$ 計算出來 ```cpp #define N 100 #define complex_t complex<double> #define pi 3.1415926535892 complex_t w[N]; void init(int n) { complex_t delta = complex_t(cos(2.0 * pi / n), sin(2.0 * pi / n)); w[0] = 1; for (int i = 1; i < n; i++) w[i] = w[i - 1] * delta; } ``` #### Recursion `fft(n, l, step, flag)` 表示共 $n$ 個數要做 FFT,且這些數分別為 $a_{\small l}, a_{\small l + s}, \dots, a_{\small l+(n-1)s}$ 先遞迴處理 $a_{\small l}, a_{\small l + 2s}, \dots, a_{\small l+(n-2)s}$ 和 $a_{\small l+s}, a_{\small l + 3s}, \dots, a_{\small l+(n-1)s}$ 再 $O(m)$ 列舉 $k$ 計算出 $a_{\small l+ks}, a_{\small l+(k+m)s}$ flag 用來標示做的是 DFT 還是 IDFT $\large \omega_{\normalsize n}^{\normalsize k} = \omega_{\normalsize nN/n}^{\normalsize kN/n} = \omega_{\normalsize N}^{\normalsize kN/n} = \omega_{\normalsize N}^{\normalsize ks}$ ```cpp complex_t a[N], b[N]; void fft(int n, int l, int s, bool flag = true) { if (n == 1) return; int m = n >> 1; fft(m, l, s << 1, flag); fft(m, l + s, s << 1, flag); for (int k = 0; k < n; k++) b[l + s * k] = a[l + s * k]; // buffer for (int k = 0; k < m; k++) { complex_t y = b[l + s * (k << 1)]; complex_t z = (flag ? w[k * s] : conj(w[k * s])) * b[l + s * ((k << 1) + 1)]; a[l + s * k] = y + z; a[l + s * (k + m)] = y - z; } } ``` #### Iterative 觀察遞迴的順序,不斷奇偶分割的情況下,數字會以項數的二進位後綴相同程度分在同一組。 將它們的二進位反轉,會發現每次分在同一組的數字都是連續的一段區間。 也就是說,最底層即是把 $a[i]$ 放在第 $\text{bit_reverse}(i)$ 項。 若事先將它們的順序安排好,那麼我們從下到上直接 merge 上去即可。 ![](https://i.imgur.com/fkOOO7J.png) 實作 $\text{bit_reverse}()$ 的方式有很多種,下面的作法是 $i$ 從最低位 $+1$,$j$ 從最高位做 $+1$ `for (int l = (n >> 1); (j ^= l) < l; l >>= 1);` 從最高位開始向右尋找第一個 $0$,把過程遇到的 $1$ 全部翻成 $0$ 這件事如果從最低位開始往左做,是不是跟 $+1$ 一模一樣呢? 為了只讓它們交換一次,當有一方大於(或小於)另一方時才進行 $\text{swap}$ 從寬度 $2, 4, \dots, n$,慢慢向上迭代,一些神奇的 index 就讓你們自己想怎麼填XD 而改值的順序可以省掉一個 buffer 的陣列! ```cpp void fft(int n, complex_t a[], bool inv=false) { init(n); // bit_reverse() for (int i = 1, j = 0; i < n; i++) { for (int l = (n >> 1); (j ^= l) < l; l >>= 1); if (i > j) swap(a[i], a[j]); } for (int i = 2; i <= n; i <<= 1) { int m = i >> 1; for (int j = 0; j < n; j += i) { for (int k = 0; k < m; k++) { complex_t z = (inv ? conj(w[n / i * k]) : w[n / i * k]) * a[j + k + m]; a[j + k + m] = a[j + k] - z; a[j + k] += z; } } } if (inv) for (int i = 0; i < n; i++) a[i] /= n; } ``` ### 大數乘法實作 ```cpp void str2cpx(string a, c_t b[], int n) { int len = a.size(), j = 0; for (int i = len - 1; i >= 0; i--) b[j++] = a[i] - '0'; while (j < n) b[j++] = 0; } string cpx2str(int n, complex_t a[]) { vector <int> tmp; tmp.resize(n); for (int i = 0; i < n - 1; i++) { int x = round(a[i].real()); a[i + 1] += x / 10; tmp[i] = x % 10; } tmp[n - 1] = round(a[n - 1].real()); while (!tmp.back()) tmp.pop_back(); string s = ""; while (!tmp.empty()) s += (char)(tmp.back() + '0'), tmp.pop_back(); return s; } string mul(string a_, string b_) { int n = 1; while (n < a_.size() + b_.size() - 1) n <<= 1; str2cpx(a_, a, n); str2cpx(b_, b, n); fft(n, a); fft(n, b); for (int i = 0; i < n; i++) a[i] *= b[i]; fft(n, a, 1); return cpx2str(n, a); } ``` ### 數論變換 Number Theory Transform FFT 用到了單位根 $\omega$ 的四個性質 1. $\omega_n^n = 1$ 2. $\omega_n^0, \omega_n^1, \dots, \omega_n^{n-1}$ 互不相同,這樣逆變換才存在 3. $\omega_n^2 = \omega_{(n/2)}, \omega_n^{(n/2) + k} = - \omega_n^k$,這使得奇偶分類後能把代入的值減半,問題規模也減半 4. $\displaystyle\sum_{k = 0}^{n - 1}\left(\omega_n^{j - i}\right)^k = \begin{cases} 0, \quad i \ne j \\ n, \quad i = j \end{cases}$,這點保證可以使用相同的方法做逆變換 FFT 可能出現精度問題,於是我們開始尋找是否有其他好的取樣點
{"metaMigratedAt":"2023-06-15T23:44:17.001Z","metaMigratedFrom":"YAML","title":"FFT","breaks":true,"contributors":"[{\"id\":\"60f87ada-c8bc-4f5d-9b91-2a0d3103440d\",\"add\":9162,\"del\":19}]"}
    241 views