问题引入
现在有两个多项式: \(A(x) = \sum _{i = 0}^{n - 1} a_i x^i\) 和 \(B(x) = \sum _{i = 0}^{m - 1} b_i x^i\),我们的目的是要求这两个多项式相乘后得到的多项式 \(C(x)\), \(C(x) = \sum _{i = 0}^{n + m - 1} c_i x^i\)。
初步分析
要求解 \(C(x)\),就是要求得 \(C(x)\) 每一项的系数 \(c_i\)。
若暴力的计算 \(c_i\),那么 \(c_i = \sum _{j = 0}^i a_j b_{i - j}\),总的时间复杂度是 \(O(nm)\) 的。
我们知道,直角坐标系上 \(n\) 个不同点可以确定一个过这 \(n\) 个点的 \(n - 1\) 次多项式。也就是说对于 \(C(x)\) 来讲,只要确定了它经过的 \(n + m\) 个不同的点的坐标,我们就有办法确定 \(C(x)\) 这个多项式。于是我们取 \(n + m\) 个不同的点 \(\{ (x_0, C(x_0)), (x_1, C(x_1)), \dots , (x_{n + m - 1}, C(x_{n + m - 1})) \}\),其中 \(C(x_i) = A(x_i)B(x_i)\),即我们要在 \(A(x)\) 和 \(B(x)\) 上分别取 \(n + m\) 个点。如果暴力地取点时间复杂度还是 \(O(n^2)\),而且我们还不知道当取完点后如何同过这些点确定 \(C(x)\)。
若果有方法能够快速地取点(DFT)并快速地根据点来确定多项式(IDFT),那我们就解决了问题。
取点——DFT 离散傅里叶变换
此处用“取点”表示其他博文中的“将系数表达式映射到点值表达式”(因为我感觉这个过程就是在取点)
DFT 的定义
(因为光看FFT代码搞不清FFT与傅里叶变换的关系,于是有了这一部分)
离散傅里叶变换将长度为 \(n\) 的时域信号 \(x[t]\) 转换为频域信号 \(X[k]\),其数学表达式为:
其中:
- \(x[t]\) 表示时域信号的第 \(t\) 个采样点
- \(X[k]\) 表示频域信号的第 \(k\) 个频率分量
那这坨公式和我们的多项式有什么关系呢?
抛开定义中的物理意义,假设有个多项式 \(P(u) = \sum_{t = 0} ^{n - 1} x[t] u^t\)(也就是说把定义中的时域信号看作是多项式的系数),于是 \(P(e^{-i{2 \pi \over n}k}) = \sum_{t = 0} ^{n - 1} x[t] (e^{-i{2 \pi \over n}k})^t = X[k]\)。此时对多项式取点的操作就与 DFT 产生了联系:对于一个 \(n - 1\) 次多项式,我们可以对其进行 DFT 以求得这个多项式在 \(e^{-i{2 \pi \over n}0},e^{-i{2 \pi \over n}1},e^{-i{2 \pi \over n}2}, \dots ,e^{-i{2 \pi \over n}(n - 1)}\) 这 \(n\) 个位置上的值,这 \(n\) 个点其实就是单位根
但是如果按照定义直接每个点单独求值,时间复杂度还是 \(O(n^2)\) 的,并没有什么优化,于是就有 FFT。
(PS:在问题分析中我们提到要在 \(A(x)\) 和 \(B(x)\) 上分别取 \(n + m\) 个点,为了解决这个问题,我们可以在两个多项式后面添项,如:\(A(x) = (\sum _{i = 0}^{n - 1} a_i x^i) + 0 \times x^n + \dots + 0 \times x^{n + m - 1}\))
FFT——DFT 的高效实现
(观看视频 BV1za411F76U 可以比较清楚地知道 FFT 的实现。)
FFT 是 DFT 数学公式的高效实现,通过分治和对称性减少计算量。
我们将原本的多项式(这里是添项过后的,而且项数 \(n\) 是 2 的幂) \(A(x) = \sum _{i = 0}^{n - 1} a_i x^i\) 的系数分成两部分,分别构成两个新的多项式 \(A_e(x) = \sum _{i = 0}^{\frac{n}{2} - 1} a_{2i} x^i\) 和 \(A_o(x) = \sum _{i = 0}^{\frac{n}{2} - 1} a_{2i + 1} x^i\)。(角标的含义是 even 和 odd)。
则 \(A(x)\) 还可以这样表示:
那么代入单位根 \(w_n^k\) 得到:
而且我们不需要从 0 到 \(n - 1\) 取枚举 \(k\),因为对于 \(k < {n \over 2}\),有 \(w_n^{k + {n \over 2}} = -w_n^k\),即:
所以我们只需要枚举前一半就好了。
对于 \(A_e(w_{n \over 2}^k)\) 和 \(A_o(w_{n \over 2}^k)\),同样用 FFT,递归的求就可以了,当项数为 1 时直接返回系数或者什么都不做就好了。
于是就可以写出递归版的 FFT (代码等说了 IDFT 再给出)
确定多项式——IDFT 离散傅里叶逆变换
在 DFT 中我们实际是解了一个这样的方程:
即求解了 \(A(w_n^i)\)
那么在 IDFT 中我们知道了 \(A(w_n^i)\) 要求 \(a_i\)。在方程两边同时左乘第一个矩阵的逆矩阵(求逆矩阵直接搜范德蒙矩阵的逆矩阵就好了,虽然有点区别,但直接类比过来就好了)得到:
这几乎是和 DFT 一样的问题,只不过 IDFT 中的单位根要和 DFT 中的单位根共轭,并且最后还要除 \(n\)。
于是就可以得到递归版的代码:
// len表示项数,并且一定是 2 的幂
void fft(int len, std::vector<std::complex<double>> &A, bool inv) {if (len == 1) {return;}int ll = len >> 1;std::vector<std::complex<double>> Ae(ll), Ao(ll);for (int i = 0; i + i < len; i++) {Ae[i] = A[i << 1];Ao[i] = A[i << 1 | 1];}fft(ll, Ae, inv);fft(ll, Ao, inv);double angle = 2.0 * M_PI / double(len);std::complex<double> unit(cos(angle), (inv ? -1 : 1) * sin(angle)), w(1.0, 0.0);for (int i = 0; i < ll; i++, w *= unit) {std::complex<double> tmp = w * Ao[i];A[i] = Ae[i] + tmp;A[i + ll] = Ae[i] - tmp;}// 当使用 IDFT 时,别忘了在函数外边另外除项数。return;
}
有了最基础递归版的 FFT,就可以将 P3803 【模板】多项式乘法(FFT) 解决了。(题解里面有挺多很好的题解的)
(迭代版的 FFT 和 NNT 之后再学,逃~)