引入
假设我们有两个多项式:
\( A(x) = a_0 + a_1x + a_2x^2 + ... + a_{n-1}x^{n-1} \)
\( B(x) = b_0 + b_1x + b_2x^2 + ... + b_{m-1}x^{m-1} \)
我们希望计算它们的乘积:
\( C(x) = A(x) \times B(x) = c_0 + c_1x + c_2x^2 + ... + c_{(n+m-2)}x^{(n+m-2)} \)
其中,每个系数 \( c_k = \sum_{i=0}^{k} a_i b_{k-i} \)。
如果直接计算(卷积),我们需要计算大约 \( n \times m \) 次乘法和加法。如果 \( n \) 和 \( m \) 都很大,这个 \( O(n^2) \) 的复杂度会非常慢。
而快速傅里叶变换(FFT)提供了一种将复杂度降至 \( O(n \log n) \) 的方法。
变换
多项式的标准表示法是系数表示法(Coefficient Representation),即我们上面用的 \( (a_0, a_1, ..., a_{n-1}) \)。还有一种等价的表示法,叫做点值表示法(Point-Value Representation)。
众所周知,两点唯一确定一条直线。那么三个点,四个点,甚至更多点呢?已有证明,一个 \( n-1 \) 次多项式可以由其在 \( n \) 个不同点 \( (x_0, x_1, ..., x_{n-1}) \) 上的取值 \( (A(x_0), A(x_1), ..., A(x_{n-1})) \) 唯一确定,咱知道就好。
点值表示法下的多项式乘法变得极其简单: 假设我们有多项式 \( A(x) \) 和 \( B(x) \)。
- 为 \( A(x) \) 取 \( N \) 个点:\( (x_k, A(x_k)) \),\( k=0,1,...,N-1 \)
- 为 \( B(x) \) 取同样的 \( N \) 个点:\( (x_k, B(x_k)) \),\( k=0,1,...,N-1 \) 注意,这里 \( N \) 必须至少是 \(deg(A) + deg(B) - 1 \),否则乘积多项式 \( C(x) \) 无法被唯一确定。
- 那么乘积 \( C(x) = A(x) \times B(x) \) 在这 \( N \) 个点上的值就是: \( (x_k, C(x_k)) = (x_k, A(x_k) \times B(x_k)) \),\( k=0,1,...,N-1 \)
此时点值相乘只需要 \( O(N) \) 的时间!这可比 \( O(N^2) \) 好多了。
于是,整个多项式乘法的策略就变成了:系数表示 → 点值表示 → 点值相乘 → 系数表示
- 求值(Evaluation):将 \( A(x) \) 和 \( B(x) \) 从系数形式转换为点值形式。
- 点乘(Pointwise Multiplication):计算 \( C(x_k) = A(x_k) \times B(x_k) \)。
- 插值(Interpolation):将 \( C(x) \) 从点值形式转换回系数形式。
现在的核心问题是:如何快速地在系数表示和点值表示之间进行转换? 如果随便选 \( N \) 个点,第一步(求值)和第三步(插值)的复杂度仍然是 \( O(N^2) \)(因为计算每个 \( A(x_k) \) 需要 \( O(N) \) 时间)。
FFT 的精妙就在于它精心选择了一组特殊的点,并利用分治策略,将求值和插值的过程都优化到了 \( O(N \log N) \)。
DFT 与单位根
FFT 选择的这组特殊的点,是单位根(Roots of Unity)。
离散傅里叶变换(DFT) 就是将一个多项式 \( A(x) \) 在 \( N \) 个 \( N \)次单位根 \( \omega_N^0, \omega_N^1, ..., \omega_N^{N-1} \) 上进行求值,得到的结果 \( (A(\omega_N^0), A(\omega_N^1), ..., A(\omega_N^{N-1})) \) 就是其DFT系数。
\( N \)次单位根是方程 \( z^N = 1 \) 的 \( N \) 个复数解。它们可以被表示为: \( \omega_N^k = e^{2\pi i k / N} = \cos(\frac{2\pi k}{N}) + i \sin(\frac{2\pi k}{N}) \), 其中 \( k = 0, 1, ..., N-1 \),\( i \) 是虚数单位。
单位根有几个极其重要的性质,是FFT分治的基础:
- 消去引理: \( \omega_{2N}^{2k} = \omega_N^k \)
- 折半引理: 如果 \( N \) 是偶数,那么 \( \omega_N^{k + N/2} = -\omega_N^k \)
- 求和引理:稍后在逆变换中会用到。
分治的魔法
FFT 是快速计算 DFT 的算法。我们假设 \( N \) 是 2 的幂(如果不是,可以补零到最近的 2 的幂)。
我们将多项式 \( A(x) \) 按奇偶项分成两个新的、规模减半的多项式:
\( A^{[0]}(x) = a_0 + a_2x + a_4x^2 + ... + a_{N-2}x^{N/2 - 1} \) (偶数索引项)
\( A^{[1]}(x) = a_1 + a_3x + a_5x^2 + ... + a_{N-1}x^{N/2 - 1} \) (奇数索引项)
很容易发现,原多项式可以表示为: \( A(x) = A^{[0]}(x^2) + x A^{[1]}(x^2) \)
现在,我们的问题从“求 \( A(x) \) 在 \( N \) 个点 \( (\omega_N^0, \omega_N^1, ..., \omega_N^{N-1}) \) 上的值”,变成了“求 \( A^{[0]}(x) \) 和 \( A^{[1]}(x) \) 在 \( N/2 \) 个点 \( ((\omega_N^0)^2, (\omega_N^1)^2, ..., (\omega_N^{N-1})^2) \) 上的值”。
根据消去引理:\( (\omega_N^k)^2 = \omega_{N/2}^k \) 并且,\( (\omega_N^{k + N/2})^2 = (\omega_N^k \omega_N^{N/2})^2 = (\omega_N^k \cdot -1)^2 = (\omega_N^k)^2 = \omega_{N/2}^k \)
这意味着,我们需要求值的 \( N \) 个平方点,实际上只有 \( N/2 \) 个不同的值:\( \omega_{N/2}^0, \omega_{N/2}^1, ..., \omega_{N/2}^{N/2 - 1} \)!
于是,我们可以进行分治递归:
设 \( y_k^{[0]} = A^{[0]}(\omega_{N/2}^k) \) 设 \( y_k^{[1]} = A^{[1]}(\omega_{N/2}^k) \) 这些是规模为 \( N/2 \) 的DFT问题。
那么,原问题 \( A(\omega_N^k) \) 的解可以通过合并子问题的解得到:
-
对于 \( k = 0, 1, ..., N/2 - 1 \):
\( A(\omega_N^k) = A^{[0]}(\omega_{N/2}^k) + \omega_N^k \cdot A^{[1]}(\omega_{N/2}^k) = y_k^{[0]} + \omega_N^k y_k^{[1]} \)
-
对于 \( k = N/2, ..., N-1 \)(令 \( k’ = k - N/2 \)):
\( \omega_N^{k} = \omega_N^{k’ + N/2} = -\omega_N^{k’} \) \( A(\omega_N^{k}) = A(\omega_N^{k’ + N/2}) = A^{[0]}(\omega_{N/2}^{k’}) + \omega_N^{k’ + N/2} \cdot A^{[1]}(\omega_{N/2}^{k’}) = y_{k’}^{[0]} - \omega_N^{k’} y_{k’}^{[1]} \)
这个过程就是FFT的核心!
复杂度分析:
- 每层递归:我们将一个规模为 \( N \) 的问题分解为 2 个规模为 \( N/2 \) 的问题,并进行 \( O(N) \) 的合并操作(乘以 \( \omega_N^k \) 并相加)。
- 递归树的高度是 \( \log_2 N \)。
- 总复杂度:\( T(N) = 2T(N/2) + O(N) = O(N \log N) \)。
IDFT
现在我们有了 \( C(x) \) 的点值表示 \( ((\omega_N^0, C(\omega_N^0)), (\omega_N^1, C(\omega_N^1)), ..., (\omega_N^{N-1}, C(\omega_N^{N-1}))) \),如何变回系数 \( (c_0, c_1, ..., c_{N-1}) \)?这个过程称为逆离散傅里叶变换(IDFT)。
神奇的是,IDFT的过程和DFT几乎一模一样。 DFT的矩阵形式是:
也就是说,计算IDFT的过程就是:
- 将点值向量 \( \vec{y} \) 作为输入。
- 将单位根 \( \omega_N^k \) 全部替换为其共轭 \( \omega_N^{-k} \)。
- 执行一遍和FFT完全相同的算法!
- 最后将得到的结果除以 \( N \)。
所以,IFFT 的复杂度也是 \( O(N \log N) \)。
小结
假设我们要计算 \( A(x) \times B(x) \),它们的次数界分别为 \( n \) 和 \( m \)。
-
补零(Zero-padding):
- 确定次数界 \( N \geq n + m - 1 \),并且 \( N \) 是 2 的幂。
- 将 \( A(x) \) 和 \( B(x) \) 的系数向量长度都补到 \( N \),后面补零。
-
求值(FFT):
- 对补零后的 \( A(x) \) 的系数向量执行FFT,得到其点值表示 \( \vec{A} = (A(\omega_N^0), A(\omega_N^1), ..., A(\omega_N^{N-1})) \)。
- 对补零后的 \( B(x) \) 的系数向量执行FFT,得到其点值表示 \( \vec{B} = (B(\omega_N^0), B(\omega_N^1), ..., B(\omega_N^{N-1})) \)。
-
点乘(Pointwise Multiply):
- 计算 \( \vec{C} = \vec{A} \circ \vec{B} \),即 \( C(\omega_N^k) = A(\omega_N^k) \times B(\omega_N^k) \) for \( k=0,1,...,N-1 \)。
-
插值(IFFT):
- 对点值向量 \( \vec{C} \) 执行逆FFT(即将单位根取共轭,做FFT,再除以N),得到的结果就是乘积多项式 \( C(x) \) 的系数向量 \( (c_0, c_1, ..., c_{N-1}) \)。
最终复杂度为 \( O(N \log N) \),远优于直接的 \( O(N^2) \)。
这就是FFT为何在信号处理、图像处理、数值计算等众多领域成为基石算法的原因。它通过巧妙的数学变换和分治策略,极大地加速了卷积(多项式乘法)运算。