快速傅里叶变换
点值表示法
对于一个 阶多项式 ,如果我们已知一个点集 ,点集 中的所有点都满足 ,且 各不相同。那么这个点集 就是多项式 的一个点值表示。
插值多项式
通过点值表达式还原多项式的操作就是插值,这等价于求解一个线性方程组:
表示成矩阵形式就是
这里系数矩阵 是范德蒙德矩阵,该矩阵的行列式值是 ,因为 各不相同,所以该矩阵的行列式值 ,即系数矩阵满秩,该线性方程组只能有唯一解。并且可以计算出多项式系数向量:
点值表示法下的多项式乘法
假设多项式 ,其中 的度数(最高次幂)为 , 的度数为 ,此时 的度数 。如果要通过点值表示法插值出原多项式 ,显然至少需要 个点。因此,多项式 的点值表示都需要 个点,设点集分别为 :
对于任意点 ,。因此,多项式 的点值表示为
利用点值表示法求多项式乘法的流程实际上就是:
- 将给定的多项式 (设度数都是 量级)分别转变为点值表示
- 通过 的点值表示计算出 的点值表示
- 通过 的点值表示插值还原出系数多项式
其中第2步的时间复杂度显然为 ,如果我们可以通过合理的点集选取,使得第一步和第三步都能够在 的复杂度下实现,那么我们就得到了一个总复杂度为 的多项式乘法算法。
快速傅里叶变换
单位复根
次单位复根 是满足 的复数 ,这样的复根恰好有 个:。根据欧拉公式可知,这些复根均匀地分布在复平面的单位圆上。

一些引理
消去引理
对任意整数 以及 ,
从几何意义考虑,这个引理是显然的,并且由此可以得出一个推论:。
折半引理
如果 是偶数,那么 个 次单位复根的平方的集合就是 个 次单位复根的集合。
证明:根据消去引理有 。并且注意到,如果对所有 次单位复根进行平方,那么获得每个 次单位根正好 次,因为
即 和 的平方相同。
求和引理
对任意整数 和不能被 整除的非负整数 ,有
离散傅里叶变换(DFT)
回想一下我们的问题:计算多项式乘法 。为了 解决这个问题,我们需要将系数表示转换为点值表示,这里我们对于点值表示法的选取就是 次单位复根,即我们希望计算多项式
在 处的值(即 个 次单位复根处)。假设 的系数表示用向量 表示,定义:
则向量 就是系数向量 的离散傅里叶变换(DFT),记作 。
这里的公式可以与傅里叶变换文中的离散傅里叶变换公式作比较,不难发现本质上是相同的。
快速傅里叶变换(FFT)
本章节中,假设 是 的整数次幂。
**快速傅里叶变换(FFT)**实际上就是利用 次单位复根的性质,在 的时间复杂度下计算出 阶多项式 的点值表示。按照下标的奇偶性,分别定义两个多项式 :
原多项式 。
比如 阶多项式 就处理为:
其中 。于是 。
于是,求 在 处的值就转化为:求两个新的多项式 在 处的值,根据折半引理可知这 个点的点值实际上只有 个不同的取值(因为 ),这就使得子问题的量级减半了,即求 转化为了两个 的子问题,时间复杂度如下:
快速傅里叶逆变换(Inverse FFT)
求出多项式的点值表示后,我们还需要将点值表示逆运算为常见的系数表示,这一步就是离散傅里叶逆变换(IDFT)。前文中,我们已经提到了IDFT就是插值,即解方程
在FFT中,点值表示选用了 次单位复根,上方的方程可以进一步写成
这个单位复根构成的逆矩阵可以表示为以下形式:
我们记 ,只需要验证 ,这里略去计算过程。
因此有
与公式 相比较,不难发现这两个问题几乎是一样的(计算 只需要将向量 互换,单位复根 取逆)。
算法实现
分治
假设我们已知 (这里 分别是两个长度为 的向量),求解 。也就是我们已知 和 ,求 。
根据前文推导的公式 可知 时:
而后一半的计算则略有不同():
然后就可以写出FFT的代码了:
using cp = complex<double>;void fft(vector<cp>& a, int inv) { int n = a.size(); if (n == 1) return; vector<cp> a0(n / 2), a1(n / 2); for (int i = 0; i * 2 < n; i++) { a0[i] = a[i * 2]; a1[i] = a[i * 2 + 1]; } fft(a0, inv); fft(a1, inv);
double angle = 2 * pi / n * (inv ? -1 : 1); cp w(1, 0), wn(cos(angle), sin(angle)); for (int i = 0; i * 2 < n; i++) { a[i] = a0[i] + w * a1[i]; a[i + n / 2] = a0[i] - w * a1[i]; if (inv) { a[i] /= 2; a[i + n / 2] /= 2; } w *= wn; }}上方函数中 inv 取 时就是一次FFT的正变换;否则是FFT的逆变换。
下面是模板题SPOJ - POLYMUL的一个AC代码:
#include <bits/stdc++.h>using namespace std;using db = double;const db pi = acos(-1);using cp = complex<db>;
void fft(vector<cp>& a, int inv) { int n = a.size(); if (n == 1) return; vector<cp> a0(n / 2), a1(n / 2); for (int i = 0; i * 2 < n; i++) { a0[i] = a[i * 2]; a1[i] = a[i * 2 + 1]; } fft(a0, inv); fft(a1, inv);
db angle = 2 * pi / n * (inv ? -1 : 1); cp w(1, 0), wn(cos(angle), sin(angle)); for (int i = 0; i * 2 < n; i++) { a[i] = a0[i] + w * a1[i]; a[i + n / 2] = a0[i] - w * a1[i]; if (inv) { a[i] /= 2; a[i + n / 2] /= 2; } w *= wn; }}
vector<long long> multiply(vector<int>& a, vector<int>& b) { int n = 1; vector<cp> fa(a.begin(), a.end()), fb(b.begin(), b.end()); while (n < a.size() + b.size()) n <<= 1; fa.resize(n); fb.resize(n);
fft(fa, 0), fft(fb, 0);
for (int i = 0; i < n; i++) fa[i] *= fb[i];
fft(fa, 1); vector<long long> res(n); for (int i = 0; i < n; i++) res[i] = round(fa[i].real()); return res;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); int t; cin >> t; while (t--) { int n; cin >> n; vector<int> a(n + 1), b(n + 1); for (auto& i : a) cin >> i; for (auto& i : b) cin >> i; auto c = multiply(a, b); for (int i = 0; i < n * 2 + 1; i++) cout << c[i] << " \n"[i == n * 2]; } return 0;}优化(倍增FFT)
下图展示了一个 阶多项式的系数在每一轮递归后的位置:

注意每个系数的下标在最终状态(树的叶子结点)时的位置,。我们从二进制的角度找找规律:。观察到:把原始序列下标的二进制翻转对称一下,就是最终那个位置的下标。这个性质被称为是位逆序置换(bit-reversal permutation)。
位逆序置换显然可以在 的复杂度下实现,但也有更优的 做法,设 表示二进制数 翻转后的数,从小到大递推 。
- 首先 。
- 因为我们从小到大递推 的值,因此在求 时, 的值已知。只要将 右移一位,然后翻转,再右移一位,就得到了 除了二进制最高位之外所有位的翻转结果,而 的最高位可以通过 求出。于是 这里 表示将 移动至最高位的偏移量。
代码如下:
int bit_reorder(int n, vector<cp>& a) { int len = __builtin_ctz(n); if ((int)rev.size() != n) { rev.assign(n, 0); for (int i = 0; i < n; i++) { rev[i] = (rev[i >> 1] >> 1) + ((i & 1) << (len - 1)); } } for (int i = 0; i < n; i++) { if (i < rev[i]) { swap(a[i], a[rev[i]]); } } return len;}也就是说我们能够直接找到FFT递归树最后一层的状态,然后自下而上地合并两个叶子,每一次合并实际上都是在做“已知 和 ,求 ”这个子问题,从前文已经推出了这个问题的解法:
注意到,如果我们把向量 和 依次排列,即 ,对比所求向量 ,不难发现 和 的位置两两对应,因此我们可以直接在原数组上计算傅里叶变换,无需递归。这个优化被称为是蝴蝶变换(butterfly transform)。
void fft(vector<cp>& a, int inv) { int n = a.size(); int len = bit_reorder(n, a);
for (int k = 1; k < n; k <<= 1) { db angle = 2 * pi / (k * 2) * (inv ? -1 : 1); cp wn(cos(angle), sin(angle)); for (int i = 0; i < n; i += k * 2) { cp w(1, 0); for (int j = i; j < i + k; j++) { cp yk0 = a[j], yk1 = a[j + k] * w; a[j] = yk0 + yk1; a[j + k] = yk0 - yk1; w *= wn; } } } if (inv) { for (auto& i : a) i /= n; }}下面是模板题SPOJ - POLYMUL的倍增法AC代码:
#include <bits/stdc++.h>using namespace std;using db = double;const db pi = acos(-1);using cp = complex<db>;vector<int> rev;int bit_reorder(int n, vector<cp>& a) { int len = __builtin_ctz(n); if ((int)rev.size() != n) { rev.assign(n, 0); for (int i = 0; i < n; i++) { rev[i] = (rev[i >> 1] >> 1) + ((i & 1) << (len - 1)); } } for (int i = 0; i < n; i++) { if (i < rev[i]) { swap(a[i], a[rev[i]]); } } return len;}
void fft(vector<cp>& a, int inv) { int n = a.size(); int len = bit_reorder(n, a);
for (int k = 1; k < n; k <<= 1) { db angle = 2 * pi / (k * 2) * (inv ? -1 : 1); cp wn(cos(angle), sin(angle)); for (int i = 0; i < n; i += k * 2) { cp w(1, 0); for (int j = i; j < i + k; j++) { cp yk0 = a[j], yk1 = a[j + k] * w; a[j] = yk0 + yk1; a[j + k] = yk0 - yk1; w *= wn; } } } if (inv) { for (auto& i : a) i /= n; }}
vector<long long> multiply(vector<int>& a, vector<int>& b) { int n = 1; vector<cp> fa(a.begin(), a.end()), fb(b.begin(), b.end()); while (n < a.size() + b.size()) n <<= 1; fa.resize(n); fb.resize(n);
fft(fa, 0), fft(fb, 0);
for (int i = 0; i < n; i++) fa[i] *= fb[i];
fft(fa, 1); vector<long long> res(n); for (int i = 0; i < n; i++) res[i] = round(fa[i].real()); return res;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); int t; cin >> t; while (t--) { int n; cin >> n; vector<int> a(n + 1), b(n + 1); for (auto& i : a) cin >> i; for (auto& i : b) cin >> i; auto c = multiply(a, b); for (int i = 0; i < n * 2 + 1; i++) cout << c[i] << " \n"[i == n * 2]; } return 0;}