---
title: 快速傅立葉轉換
tag: 數論
disqus: hackmd
---
# 快速傅立葉轉換 (FFT)
## 序
本篇包含大量 $\LaTeX$ 數學表達式,如果爛掉請嘗試重新載入。
~~因為讀學測讀得很累,所以換換口味~~
最近突然腦子撞到,所以上網查了大數乘法的資料,不過似乎並不是特別的豐富(~~肯定是我不會查~~),而且相關的文章對「為何這麼做的原因」的解釋都不太多,所以決定自己寫一份筆記,因為是筆記所以會寫的特別白話,希望對看這篇的人有幫助。
如果有疏漏歡迎留言指正。
這篇的目標是讓人一次就看懂 FFT,不過當然要一些國高中的先備知識。
#### 本篇會用到的數學與演算法:
- 數學
- 多項式的基本知識
- 簡單的三角函數
- 虛數(只要知道 $i^2 = -1$ 就可以了)
- [矩陣乘法](https://zh.wikipedia.org/zh-tw/矩陣乘法)
- 演算法
- 時間複雜度
- 分治
## 大數計算
如果我們需要計算的數字大於 C++ 裡 long long 的儲存範圍,那麼我們就只能用字串來存放數字,並利用模擬的方式來計算答案,不過感覺正常來說這種需求不是很多,所以主要當個切入點。
假設現在有兩個數 $A, B$,並稱兩個數字的位數是 $a, b$。
普通的加減計算很簡單,你平常怎麼算程式就怎麼寫,複雜度是 $O(a+b)$。
不過到了乘法,我們平常的計算方式所需的複雜度是 $O(ab)$。
\begin{array}{r}
111 \\
\underline{\times \ \color{red}2 \color{green}3 \color{darkorange}4} \\
\color{darkorange}{444} \\
\color{green}{333} \phantom{1} \\
\color{red}{222} \phantom{11} \\
\hline{25974}
\end{array}
這聽起來不太妙,所以我們如何優化它?
其中一個方式是把它轉化成多項式乘法。
我們知道一個整數都可以有以下的表示方法:
$$
1234 = 1 \times 10^3 + 2 \times 10^2 + 3 \times 10^1 + 4 \times 10^0
$$
這種寫法就和多項式一樣
\begin{split}
Num(x) &= 1x^3 + 2x^2 + 3x + 4 \\
Num(10) &= 1234
\end{split}
因此,我們可以**把大數乘法直接變成多項式乘法**,將多項式乘法做完之後,再把 10 代進結果中就可以得到答案了,即:
$$
\begin{split}
A(x) &= s_0 + s_1x + s_2x^2 + s_3x^3 \\
B(x) &= t_0 + t_1x + t_2x^2 + t_3x^3 \\
C(x) &= A(x) \times B(x) \\
C(10) &= A(10) \times B(10) = ans
\end{split}
$$
所以就像剛剛說的,大數乘法只是一個切入點,實際上我們在意的是**如何快速的計算出兩個多項式的乘積**。計算多項式的乘積在很多地方都有用途,而且多項式的項數可以非常多,所以計算的時間複雜度就是一個需要在意的事情了。
## 多項式乘法
在討論如何快速的多項式乘法前,我們先來看看如何表示一個多項式。
### 多項式表示法
1. 係數表示法
一個多項式 $f(x) = p_0 + p_1x + p_2x^2 + p_3x^3$,只要規定好書寫的方向(升冪或降冪),我們就可以只寫係數而不寫後面的 $x$,實際上我們在學校學綜合除法時就用過這種表示方式,讓算式變得比較乾淨,如:用 $[p_0, p_1, p_2, p_3]$ 表示上面的 $f(x)$。
這種寫法也可以稱為一個「**向量**」
2. 點值表示法
我們知道,在座標平面上,可以用兩個相異的點來決定一條唯一的直線($x$ 的一次式)
實際上,我們可以利用 $n+1$ 個點來決定一個 $x$ 的 $n$ 次多項式,所以我們也可以利用 $n+1$ 個點來表示一個多項式。
而利用這 $n+1$ 個點反推回多項式的方法稱為「**插值法**」。
:::spoiler 插值法的唯一性證明
如果我們選取的每個點都對決定這個多項式有貢獻(我們選的點都滿足線性獨立),那麼,最少只需要 $n+1$ 個點就可以決定一個唯一的多項式,因為一個 $n$ 次多項式有 $n+1$ 個係數。
$$
\begin{split}
f(x) = p_0 + p_1x + p_2x^2 + \cdots + p_nx^n
\end{split}
$$
而我們可以將這 $n+1$ 個點帶入 $f(x)$ 而得到 $n+1$ 個方程式。
$$
\begin{split}
y_1 = p_0 + p_1x_1 + &p_2x^2_1 + \cdots + p_nx^n_1 \\
y_2 = p_0 + p_1x_2 + &p_2x^2_2 + \cdots + p_nx^n_2 \\
\vdots \\
y_{n+1} = p_0 + p_1x_{n+1} + &p_2x^2_{n+1} + \cdots + p_nx^n_{n+1} \\
\end{split}
$$
藉由解這 $n+1$ 個方程式就可以解出係數了。
:::
### 多項式乘法
既然用係數表示的多項式沒辦法高效的完成乘法,那我們來看看如果多項式使用點值表示法,做乘法的效率會如何?
一樣是兩個多項式 $A(x)$ 和 $B(x)$,不過這次他們的最高次都是 $n$
那我們可以利用 $n+1$ 個點來表示這兩個多項式:
$$
\begin{matrix}
(0, A(0)) & (1, A(1)) & \cdots & (n, A(n)) \\
(0, B(0)) & (1, B(1)) & \cdots & (n, B(n)) \\
\end{matrix}
$$
而我們要的 $C(x) = A(x) \times B(x)$ 可以寫成這樣:
$$
\begin{array}{l}
(0, C(0)) & (1, C(1)) & \cdots & (n, C(n)) \\
(0, A(0) \times B(0)) & (1, A(1) \times B(1)) & \cdots & (n, A(n) \times B(n)) \\
\end{array}
$$
但是我們知道 $A(x) \times B(x)$ 後,$C(x)$ 的最高次次數是 $2n$,所以我們應該準備 $2n+1$ 個點來確定出唯一的 $C(x)$,所以應該改寫成:
$$
\begin{array}{l}
(0, C(0)) & (1, C(1)) & \cdots & \color{red}{(2n, C(2n))} \\
(0, A(0) \times B(0)) & (1, A(1) \times B(1)) & \cdots & \color{red}{(2n, A(2n) \times B(2n))} \\
\end{array}
$$
如此這般,如果利用點值來表示 $A(x)$ 和 $B(x)$,那我們的多項式乘法就可以在 $O(n)$ 的時間內做完了,可喜可賀。
所以現在的問題就變成了「**如何快速的求出點值**」和「**如何快速的將點值轉換回多項式**」
## 多項式求值
對多項式求值這件事我們在寫數學題目時在熟悉不過了,但是仔細想想就會發現,我們使用的方法時間複雜度其實非常糟糕,以下是一些求值的方法。
1. 直接一個一個算
如果對 $f(x) = p_0 + p_1x + \cdots + p_nx^n$ 直接一個一個算,而且計算 $2n+1$ 個點的值,那麼時間複雜度將會是 $O(\frac{n\times(n+1)}{2} \times (2n+1)) = O(n^3)$,非常不妙。
2. 簡單的優化
如果對 $f(x)$ 做一些簡單的合併,就會得到這個:
$$
\begin{split}
f(x) = p_0 + x(p_1 + x(p_2 + x(\cdots + x(p_{n-1} + p_nx))))
\end{split}
$$
這樣一來,求全部值的複雜度就變成了 $O(n \times (2n+1)) = O(n^2)$,可以更好嗎?
3. 使用魔法
來到本篇的重點,如果使用快速傅立葉轉換,我們可以將這個複雜度在優化到 $O(n\log{n})$
所以終於來到了這篇的主題了,什麼是「**快速傅立葉轉換**」?為何他可以快速的求值?
## 快速傅立葉轉換
快速傅立葉轉換 Fast Fourier transform 簡稱 FFT
什麼是傅立葉轉換?最簡化的理解就是**對多項式求值**,不過用的地方不一樣所以名字比較酷。
所以快速傅立葉轉換就是「快速的對多項式求值」,也就是剛剛的多項式乘法中,時間複雜度的瓶頸之處。而快速傅立葉轉換利用函數的特性,將多項式求值的複雜度從 $O(n^2)$ 降到 $O(n\log{n})$。
接著就來看看他是如何做到的
### 函數值的觀察
下面是 $f(x) = x^2$ 的函數圖形
![image](https://hackmd.io/_uploads/HyKThDHDp.png)
我們可以發現 $f(-x) = f(x)$,所以如果我們選的是這六個點,其實只需要算三次值
而對於 $x$ 項都是偶數次方的函數來說這件事都成立,這種函數叫做「**偶函數**」
而這是 $f(x) = x^3$ 的函數圖形
![image](https://hackmd.io/_uploads/SyWs3PHP6.png)
我們又可以發現 $f(-x) = -f(x)$,一樣可以減少計算次數
而對於 $x$ 項都是奇數次方的函數來說這件事都成立,這種函數叫做「**奇函數**」
### 原函數的修改
假設函數總共有 $n$ **項**($n$ 項多項式最高次為 $n-1$)
我們原來的函數長這樣:
$$
f(x) = p_0 + p_1x + p_2x^2 + \cdots + p_{n-1}x^{n-1}
$$
先假設 $n$ 是一個二的倍數,於是我們可以將他分解成一個奇函數和一個偶函數的和:
\begin{split}
f_{even}(x) &= p_0 + p_2x^2 + p_4x^4 + \cdots + p_{n-2}x^{n-2} \\
f_{odd}(x) &= p_1x + p_3x^3 + p_5x^5 + \cdots + p_{n-1}x^{n-1} \\
f(x) &= f_{even}(x) + f_{odd}(x)
\end{split}
並利用剛剛的結論:
\begin{split}
f_{even}(-x) &= f_{even}(x) \\
f_{odd}(-x) &= -f_{odd}(x) \\
\end{split}
我們就可以得到這個等式:
\begin{array}{l}
f(x) &= f_{even}(x) + f_{odd}(x) \\
f(-x) = f_{even}(-x) + f_{odd}(-x) &= f_{even}(x) \color{red}- f_{odd}(x)
\end{array}
這樣我們就只需要計算 $n$ 個 $f_{even}(x)$ 和 $f_{odd}(x)$ 就可以得到 $2n$ 個點,雖然一乘一除看起來計算次數不變,不過這一步的意義在於我們將函數分解了。
### 新函數的計算
我們現在成功的將原函式分成了兩個新的多項式,那有沒有可能繼續將新函式分解成兩個,如果可以一直分到函式只剩一項,我們就可以直接回傳常數項了。
而且這樣最多分解成 $\log n$ 個新函式,複雜度變可以下降成 $O(n\log n)$。
讓我們先來觀察兩個新函數的不同:
\begin{split}
f_{even}(x) &= p_0x^\color{red}0 + p_2x^\color{red}2 + p_4x^\color{red}4 + \cdots + p_{n-2}x^\color{red}{n-2} \\
f_{odd}(x) &= p_1x^\color{red}1 + p_3x^\color{red}3 + p_5x^\color{red}5 + \cdots + p_{n-1}x^\color{red}{n-1} \\
\end{split}
除了係數不一樣外,如果將 $f_{odd}(x)$ 的 $x$ 提出來,兩個式子就長得很像了:
\begin{split}
f_{even}(x) &= \ \ \ \ p_0x^\color{red}0 + p_2x^\color{red}2 + p_4x^\color{red}4 + \cdots + p_{n-2}x^\color{red}{n-2} \\
f_{odd}(x) &= \color{red}x(p_1x^\color{red}0 + p_3x^\color{red}2 + p_5x^\color{red}4 + \cdots + p_{n-1}x^\color{red}{n-2}) \\
\end{split}
既然兩個式子長的差不多,那接下來就以 $f_{even}(x)$ 當作例子。
我們可以發現,如果將 $k = x^2$ 對新函數做變數代換:
\begin{split}
f_{even}(k) &= p_0 + p_2k^\color{red}1 + p_4k^\color{red}2 + \cdots + p_nk^\color{red}{\frac{n}{2}-1} \\
\end{split}
就可以得到一個與原函數幾乎一模一樣的新函數。
如此一來我們就可以用分治的方法繼續分下去了$\cdots$ 嗎?
### 繼續分下去?
我們分治的地方在於每次將函數分成奇偶函數,而這樣做的前提是我們需要將代入函數的值分成**正負的點對**,如果不能做到這一點,那我們基於奇偶函數特性所創造的算法就無法成立。
然而我們剛剛對新函數 $f_{even}(x)$ 用 $k$ 進行了代換變成了 $f_{even}(k)$ ,而 $k$ 是 $x^2$ 所以 $k$ 不可能是負的,也就無法將代入 $f_{even}(k)$ 的值分成「正負點對」,除非...
除非 $x$ 是一個平方過後還會出現負數的怪物
### 對 $x$ 使用虛數吧
虛數的定義就是 $\sqrt{-1}$ 並且以 $i$ 作為代號,所以根據定義 $i^2$ 就等於 $-1$。
而我們的 $x$ 值必然會出現虛數,因為這樣才能滿足我們的需求。
不過虛數世界千千萬萬,具體來說會出現哪些虛數呢?
從原函數來看我們不好決定要代哪些虛數回去,所以不妨從遞迴的中止條件來看看哪些數比較簡單。
我們的遞迴會不斷地將原函數分成奇偶兩個,所以理所當然遞迴的中止條件就是分到最後的函數只剩一項時,我們就可以停下了,而這個只有常數項的函數,其實不管 $x$ 代什麼結果都一樣,所以我們代個最簡單的數「$1$」就可以了。
而我們是透過不斷用 $x^2$ 代換來得到新的函數,所以中止條件所代入的 $1$,在回傳的途中就會被不斷的開根號,所以最後我們原函數需要代入的 $x$ 值就是 $\sqrt[2^k]{1}$,總共 $2^k$ 個數 $(k \in \mathbb{Z})$。
:::spoiler 根號的解的個數
$\sqrt[n]{1}$ 也可以寫成 $x^n = 1$ 的解,而一個 $n$ 次多項式有 $n$ 個解,所以 $\sqrt[n]{1}$ 會有 $n$ 個解。
:::
<br/>
然而,我們原函式的項數不一定是 $2^k$ 個,所以我們必須找到一個**比項數還要大的二的冪次**,而且為了避免在遞迴途中還要一直做這件事(找冪次),所以我們可以一開始就將所求的函數補成 $2^k$ 項,我們總是可以透過將係數設為 $0$ 來做到,所以剛剛才會假設多項式的項數 $n$ 是一個二的倍數。
說了這麼多,所以什麼是 $\sqrt[2^k]{1}$,它究竟長怎樣?
### $\sqrt[2^k]{1}$ 長怎樣
我們會利用複數平面來表示一個複數, $x$ 軸是實數而 $y$ 軸是複數,這樣就可以在直角坐標系中表示出任意一個複數,而特別的是,**$\sqrt[n]{z}$ 的 $n$ 個解在複數平面上是一個圓的內接正 $n$ 邊形**。
那如何可以表示出這正 $n$ 邊形每個頂點的數呢?
在平常的 $xy$ 平面上,我們會用 $(x, y)$ 來表示出一個點,而我們也可以用三角函數來表示這個點。
假設 $r = \sqrt{x^2 + y^2}$,也就是 $(x, y)$ 到原點的距離,那這個點也可以表示成 $(r\cos{\theta}, r\sin{\theta})$,其中 $\theta$ 是 $(x, y)$ 向量與 $x$ 軸正向的夾角。
而在複數平面上我們也可以這樣做,不過 $y$ 要多寫一個 $i$ 來表示複數軸。
這樣一來,複數 $z$ 就等於 $r(\cos{\theta} + i\sin{\theta})$。
好消息是,因為我們選的 $z$ 是 $1$,所以 $r = 1$,而且這 $n$ 個點是正多邊形,所以 $\theta$ 每次增加 $\frac{2\pi}{n}$。
這樣我們就得到了解出來的 $n$ 個點:
\begin{split}
(\cos{0} + i\sin{0}), (\cos{\frac{2\pi}{n}} + i\sin{\frac{2\pi}{n}}), (\cos{\frac{2\pi(2)}{n}} + i\sin{\frac{2\pi(2)}{n}}), \cdots, (\cos{\frac{2\pi(n-1)}{n}} + i\sin{\frac{2\pi(n-1)}{n}})
\end{split}
不過這樣子的表示法看著很不優雅,所以我們可以引入歐拉公式來簡化點的表示:
\begin{split}
e^{i\theta} = \cos{\theta} + i\sin{\theta}
\end{split}
原本的點就可以表示成:
\begin{split}
e^{i\theta(0)}, e^{i\theta(1)}, e^{i\theta(2)}, \cdots, e^{i\theta(n-1)}
\end{split}
如果我們令 $w$ 等於 $e^{i\theta}$,我們就可以得到這些點的簡單寫法:
\begin{split}
w^0, w^1, w^2, \cdots, w^{n-1}
\end{split}
這些就是我們所有需要的點值了。
### 實作快速傅立葉轉換
因為我們利用分治拆解多項式,所以多項式的項數需要是 2 的冪次,所以我們一開始假設項數 $n$ 是一個二的倍數並沒有問題,而且我們總是可以透過為零的係數來增加多項式的項數。
就此,我們就分解出了完成快速傅立葉轉換的所有步驟:
![image](https://hackmd.io/_uploads/Syg0emWjU6.png)
所以就來實作吧,這次使用 python 因為比較好讀
```python=
e = 2.71828182845 # 自然常數
pi = 3.14159265358 # 圓周率
def FFT(f):
n = len(f) # n 需為 2 的冪次
if n == 1: # 如果只剩一項就直接回傳
return f
w = e ** (2*pi*1j / n) # j 為 python 的虛數單位
even, odd = f[::2], f[1::2] # 分成奇偶函數
even, odd = FFT(even), FFT(odd) # 遞迴計算
y = [0] * n
for i in range(n//2): # 合併
y[i] = even[i] + w**i * odd[i]
y[i + n//2] = even[i] - w**i * odd[i]
return y # 回傳解答
```
到此,我們將計算點值的複雜度優化到了 $O(n\log{n})$,完成了多項式乘法的第一個部分「**如何快速的求出點值**」,在進到第二個「**如何快速的將點值轉換回多項式**」之前,我們先來看看函數求值的另一種表示法。
## 矩陣表示法
在剛剛的多項式表示法中我們提到,$[p_0, p_1, p_2, \cdots, p_n]$ 這種表示法稱為向量,也就是一個一維的矩陣,所以我們剛剛的函數求值可以寫成這樣的矩陣乘法:
\begin{equation}
\begin{bmatrix}
f(x_0) \\
f(x_1) \\
f(x_2) \\
\vdots \\
f(x_{n}) \\
\end{bmatrix} =
\underbrace{
\begin{bmatrix}
1 & x_0 & x_0^2 & \cdots & x_0^n \\
1 & x_1 & x_1^2 & \cdots & x_1^n \\
1 & x_2 & x_2^2 & \cdots & x_2^n \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & x_n & x_n^2 & \cdots & x_n^n \\
\end{bmatrix}}_{\Huge{M}}
\times
\begin{bmatrix}
p_0 \\
p_1 \\
p_2 \\
\vdots \\
p_n
\end{bmatrix}
\end{equation}
這樣就可以乾淨的表示出函數求值的過程,那我們能否把「**點值轉換回多項式**」也表示成矩陣乘法?
當然可以,下面是讓點值經過一個矩陣 $m$ 的作用後得到係數:
\begin{equation}
\begin{bmatrix}
p_0 \\
p_1 \\
p_2 \\
\vdots \\
p_n
\end{bmatrix} =
{\Huge{m}}
\times
\begin{bmatrix}
f(x_0) \\
f(x_1) \\
f(x_2) \\
\vdots \\
f(x_{n}) \\
\end{bmatrix}
\end{equation}
而這個 $m$ 就是 $M$ 的反矩陣 $M^{-1}$
:::spoiler 什麼是反矩陣
在平常的數學運算 $a = b\times c$ 我們可以用 $a / b$ 來求出 $c$,但是在矩陣中我們無法定義「除法」,而 $a / b$ 也可以寫成 $a \times b^{-1}$,所以我們用類似倒數的方法來將除法變成乘法(除以 $n$ 就是乘以 $\frac{1}{n}$ 嘛)
如果定義 $I$ 為單位矩陣(可以理解成矩陣的 $1$ :$IV = V$),那 $M$ 和 $M^{-1}$ 的關係就是:
\begin{split}
MM^{-1} = I
\end{split}
**簡單理解 $M^{-1}$ 就是一個矩陣 $M$ 的倒數,與 $M$ 相乘後就會兩個一起消失**。
:::
<br/>
所以我們傅立葉轉換的矩陣乘法寫成這樣
\begin{equation}
\begin{bmatrix}
f(w^0) \\
f(w^1) \\
f(w^2) \\
\vdots \\
f(w^{n-1}) \\
\end{bmatrix} =
\begin{bmatrix}
1 & 1 & 1 & \cdots & 1 \\
1 & w & w^2 & \cdots & w^{n-1} \\
1 & w^2 & w^4 & \cdots & w^{2(n-1)} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & w^{n-1} & w^{2(n-1)} & \cdots & w^{(n-1)(n-1)} \\
\end{bmatrix}
\times
\begin{bmatrix}
p_0 \\
p_1 \\
p_2 \\
\vdots \\
p_{n-1}
\end{bmatrix}
\end{equation}
在我們對 $A(x)$ 和 $B(x)$ 都求出值並乘出 $C(x)$ 後,我們可以利用反矩陣求回多項式的係數
\begin{equation}
\begin{bmatrix}
p_0 \\
p_1 \\
p_2 \\
\vdots \\
p_{n-1}
\end{bmatrix} =
\begin{bmatrix}
1 & 1 & 1 & \cdots & 1 \\
1 & w & w^2 & \cdots & w^{n-1} \\
1 & w^2 & w^4 & \cdots & w^{2(n-1)} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & w^{n-1} & w^{2(n-1)} & \cdots & w^{(n-1)(n-1)} \\
\end{bmatrix}^{-1}
\times
\begin{bmatrix}
C(w^0) \\
C(w^1) \\
C(w^2) \\
\vdots \\
C(w^{n-1}) \\
\end{bmatrix}
\end{equation}
好消息是,對於這個每列都是等比數列的大矩陣有個特殊的名字叫「范德蒙矩陣」,而他的反矩陣可以用公式簡單的求出來,這裡就直接給出結果:
\begin{equation}
\begin{bmatrix}
1 & 1 & 1 & \cdots & 1 \\
1 & w & w^2 & \cdots & w^{n-1} \\
1 & w^2 & w^4 & \cdots & w^{2(n-1)} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & w^{n-1} & w^{2(n-1)} & \cdots & w^{(n-1)(n-1)} \\
\end{bmatrix}^{-1} =
\frac{1}{n}
\begin{bmatrix}
1 & 1 & 1 & \cdots & 1 \\
1 & w^{-1} & w^{-2} & \cdots & w^{-(n-1)} \\
1 & w^{-2} & w^{-4} & \cdots & w^{-2(n-1)} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & w^{-(n-1)} & w^{-2(n-1)} & \cdots & w^{-(n-1)(n-1)} \\
\end{bmatrix}
\end{equation}
所以可以得到:
\begin{equation}
\begin{bmatrix}
p_0 \\
p_1 \\
p_2 \\
\vdots \\
p_{n-1}
\end{bmatrix} =
\frac{1}{n}
\begin{bmatrix}
1 & 1 & 1 & \cdots & 1 \\
1 & w^{-1} & w^{-2} & \cdots & w^{-(n-1)} \\
1 & w^{-2} & w^{-4} & \cdots & w^{-2(n-1)} \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
1 & w^{-(n-1)} & w^{-2(n-1)} & \cdots & w^{-(n-1)(n-1)} \\
\end{bmatrix}
\times
\begin{bmatrix}
C(w^0) \\
C(w^1) \\
C(w^2) \\
\vdots \\
C(w^{n-1}) \\
\end{bmatrix}
\end{equation}
這個矩陣乘法看起來跟我們原本 FFT 的矩陣乘法一模一樣,只是 $w$ 變成了 $w^{-1}$,最後在每項乘以 $\frac{1}{n}$
所以我們完全可以直接修改剛剛 FFT 的程式中的 $w$ 值就可以做到「**快速的將點值轉換回多項式**」。
## Inverse FFT
因為是以 FFT 的反矩陣來構成的算法,所以叫做 Inverse FFT,也就是反向的 FFT,我們只需修改 $w$ 就可以了,並在最後將每項都乘以 $\frac{1}{n}$,其他程式碼都不變。
時間複雜度一樣是 $O(n\log{n})$。
```python=
e = 2.71828182845 # 自然常數
pi = 3.14159265358 # 圓周率
def Inv_FFT(y, original = True):# 傳入函數值 y
n = len(y)
if n == 1:
return y
w = e ** (2*pi*1j / n)
w = 1 / w # 新的 w
# or you can write w = e ** (-2*pi*1j / n)
even, odd = y[::2], y[1::2]
even, odd = Inv_FFT(even, False), Inv_FFT(odd, False)
f = [0] * n
for i in range(n//2):
f[i] = even[i] + w**i * odd[i]
f[i + n//2] = even[i] - w**i * odd[i]
if original: # 如果回傳最終答案需逐項除以 n
f = [x/n for x in f]
return f # 回傳多項式係數 f
```
就此,我們完成了多項式乘法的優化,將時間複雜度降到了 $O(n\log{n})$,可喜可賀。
:::spoiler 完整的多項式乘法 code
```python=
e = 2.71828182845 # 自然常數
pi = 3.14159265358 # 圓周率
def FFT(f):
n = len(f) # n 需為 2 的冪次
if n == 1: # 如果只剩一項就直接回傳
return f
w = e ** (2*pi*1j / n) # j 為 python 的虛數單位
even, odd = f[::2], f[1::2] # 分成奇偶函數
even, odd = FFT(even), FFT(odd) # 遞迴計算
y = [0] * n
for i in range(n//2): # 合併
y[i] = even[i] + w**i * odd[i]
y[i + n//2] = even[i] - w**i * odd[i]
return y # 回傳解答
def Inv_FFT(y, original = True):# 傳入函數值 y
n = len(y)
if n == 1:
return y
w = e ** (2*pi*1j / n)
w = 1 / w # 新的 w
# or you can write w = e ** (-2*pi*1j / n)
even, odd = y[::2], y[1::2]
even, odd = Inv_FFT(even, False), Inv_FFT(odd, False)
f = [0] * n
for i in range(n//2):
f[i] = even[i] + w**i * odd[i]
f[i + n//2] = even[i] - w**i * odd[i]
if original: # 如果回傳最終答案需逐項除以 n
f = [x/n for x in f]
return f # 回傳多項式係數 f
# or you can combine FFT and Inv_FFT in a single function
# def FFT(f, inverse = False, original = True):
# n = len(f)
# if n == 1:
# return f
# w = e ** (2*pi*1j / n)
# if inverse: w = 1 / w
# even, odd = f[::2], f[1::2]
# if inverse: even, odd = FFT(even, True, False), FFT(odd, True, False)
# else: even, odd = FFT(even), FFT(odd)
# y = [0] * n
# for i in range(n//2):
# y[i] = even[i] + w**i * odd[i]
# y[i + n//2] = even[i] - w**i * odd[i]
# if inverse and original:
# y = [x/n for x in y]
# return y # 回傳解答
if __name__ == "__main__":
# 輸入多項式
a = list(map(complex, input().split()))
b = list(map(complex, input().split()))
# 相乘後的項數
n = len(a) + len(b) - 1
# 將項數補成 2 的冪次個
x = 1
while x < n: x *= 2
a += [complex(0)] * (x - len(a))
b += [complex(0)] * (x - len(b))
# print(len(a))
# FFT 求值
a = FFT(a)
b = FFT(b)
# 多項式乘法
c = [a[i] * b[i] for i in range(x)]
# Inverse FFT 求係數
c = Inv_FFT(c)
# c = FFT(c, inverse=True)
# 輸出四捨五入後的整數係數
for i in c:
print(round(i.real), end=' ')
```
:::
## 大數計算
終於,在經過前面的煉獄後,我們終於可以來寫大數乘法的 code 了
既然剛剛實作 FFT 是用 Python,那我們也用 Python 來實作大數乘法吧
根據時間複雜度,我們假設輸入最多有 $10^6$ 位
```python=
import sys
sys.set_int_max_str_digits(2000000)
x = int(input())
print(x * x)
```
好ㄟ,實作完了,那同學們我們這節課就到這
~~python 你好噁(抱頭痛哭~~
## 參考資料
[The Fast Fourier Transform (FFT): Most Ingenious Algorithm Ever?](https://youtu.be/h7apO7q16V0?si=TVuPRhL3wRtNAi_9)
[大数乘法(快速傅立叶变换)上](https://blog.csdn.net/u013351484/article/details/48739415)
[大数乘法(快速傅立叶变换)下](https://blog.csdn.net/u013351484/article/details/48809943)