前置知识

  • 复数运算
  • 单位根

简介

快速傅里叶变换(Fast Fourier Transform,FFT)是一种用于在 \(O(n \log n)\) 时间内计算离散傅里叶变换(Discrete Fourier Transform,DFT)的算法。在算法竞赛中,常用于快速计算多项式卷积。

朴素算法

例如有两个多项式,\(F(x),G(x)\)

\(F(x)\) 次数为 \(n\)\(G(x)\) 次数为 \(m\)

显然,要求他们的卷积 \(H(x) = F(x) \ast G(x)\),可以用 \(O(nm)\)(即 \(n^2\) 级别)的复杂度解决:

\[ H(k) = \sum_{i=0}^{k} F(i)G(k-i) \]

但,这实在太慢了,而目前看来都没有什么很好的做法,我们不妨改变一下多项式的表示方法。

点值表示法

平时我们用的 \(F(x) = a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}x^{n-1}\) 被称作 系数表示法,它可以用一个向量来描述:

\[ \mathbf{a} = \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} \]

现在,我们可以取 \(n\) 个互异的 \(x\) 值(记为 \(x_0, \dots, x_{n-1}\)),并计算出对应的 \(F(x)\) 值,那么,就构成了 \(n\)\((x_i, F(x_i))\) 的点对。这就是多项式的 点值表示法,它有许多特点:

  • 唯一性:对于次数 \(< n\) 的多项式,任取 \(n\) 组互异点对,多项式唯一确定。
  • 方便计算
    • 加法:\((F+G)(x_i) = F(x_i) + G(x_i)\),复杂度 \(O(n)\)
    • 乘法:\((F\cdot G)(x_i) = F(x_i)G(x_i)\),复杂度 \(O(n)\)

瓶颈分析

虽然点值表示法下的乘法很快,但是:

  1. 系数转点值(求值):如果我们随意取 \(n\) 个互异的 \(x\) 值,代入计算一个点的值需要 \(O(n)\)(秦九韶算法),计算 \(n\) 个点则总共需要 \(O(n^2)\) 的复杂度。
  2. 点值转系数(插值):从点值推回系数,一般需要 \(O(n^2)\) 的复杂度(拉格朗日插值)。

如果只用朴素的方法,总复杂度依然是 \(O(n^2)\),并没有变快。 我们需要一种特殊的取点方式,使得“求值”和“插值”都能在 \(O(n \log n)\) 内完成,这就是 FFT 的作用。

离散傅里叶变换 (DFT)

专业地说:

离散傅里叶变换(Discrete Fourier transform,DFT)是傅里叶变换在时域和频域上都呈离散的形式。 —— OI Wiki

在算法竞赛中,我们可以简单理解为:它是一种选取特殊的点(单位根),将 系数表示法 转到 点值表示法 的变换。

它是一种 线性 变换,可以用矩阵乘法来表示。

设系数向量为 \(\mathbf{a}\),变换后的点值向量为 \(\mathbf{y}\),则 \(\mathbf{y} = V \mathbf{a}\)

\[ \begin{bmatrix}y_0 \\y_1 \\y_2\\\vdots \\y_{n-1}\end{bmatrix}=\begin{bmatrix}1 & 1 & 1 & \cdots & 1 \\1 & \omega & \omega^2 & \cdots & \omega^{n-1} \\1 & \omega^2 & \omega^4 & \cdots & \omega^{2(n-1)} \\\vdots & \vdots & \vdots & \ddots & \vdots \\1 & \omega^{n-1} & \omega^{2(n-1)} & \cdots & \omega^{(n-1)(n-1)}\end{bmatrix}\begin{bmatrix}a_0 \\a_1 \\a_2 \\\vdots \\a_{n-1}\end{bmatrix} \]

其中,\(\omega = \omega_n = e^{i\frac{2\pi}{n}}\)\(n\) 次单位根)。

快速傅里叶变换 (FFT)

快速傅里叶变换(Fast Fourier Transform,FFT),并不是一种新的变换,而是利用单位根的性质(周期性、对称性),高效计算上述 DFT 矩阵乘法的算法。本质思想是分治

分治法

FFT 的核心思想是分治。我们就 \(n\)\(2\) 的整数幂的情况进行讨论(若不足则补 0)。

我们的目标是计算 \(x\) 取所有单位根 \(\omega_n^k\) (\(k=0, \dots, n-1\)) 时 \(F(x)\) 的值。

对于多项式: \[ F(x) = \sum_{i=0}^{n-1} a_i x^i \]

我们将系数按照下标的 奇偶性 分类:

  • 偶数项系数:\(a_0, a_2, a_4, \dots\)
  • 奇数项系数:\(a_1, a_3, a_5, \dots\)

\(F(x)\) 重写为: \[ F(x) = (a_0 + a_2 x^2 + \dots + a_{n-2}x^{n-2}) + (a_1x + a_3x^3 + \dots + a_{n-1}x^{n-1}) \]

在奇数项部分提公因式 \(x\)\[ F(x) = (a_0 + a_2 x^2 + \dots) + x(a_1 + a_3 x^2 + \dots) \]

如果我们定义两个新的多项式 \(G(x)\)\(H(x)\),大小仅为原来的一半(项数为 \(n/2\)): \[ \begin{align*} G(x) &= a_0 + a_2 x + a_4 x^2 + \dots = \sum_{i=0}^{n/2 - 1} a_{2i} x^i \\ H(x) &= a_1 + a_3 x + a_5 x^2 + \dots = \sum_{i=0}^{n/2 - 1} a_{2i+1} x^i \end{align*} \]

那么原多项式就可以表示为: \[ F(x) = G(x^2) + x \cdot H(x^2) \]

这是 FFT 最关键的一步。 我们可以利用单位根的性质,将求 \(F\)\(n\) 个点值的问题,转化为求 \(G\)\(H\)\(n/2\) 个点值的问题。

对于 \(k < \frac{n}{2}\),我们要计算 \(F(\omega_n^k)\)\(F(\omega_n^{k + n/2})\)

  1. 代入 \(x = \omega_n^k\) 利用性质 \(\omega_n^{2k} = \omega_{n/2}^k\)(折半引理): \[ \begin{align*} F(\omega_n^k) &= G((\omega_n^k)^2) + \omega_n^k \cdot H((\omega_n^k)^2) \\ &= G(\omega_n^{2k}) + \omega_n^k \cdot H(\omega_n^{2k}) \\ &= G(\omega_{n/2}^k) + \omega_n^k \cdot H(\omega_{n/2}^k) \end{align*} \]

  2. 代入 \(x = \omega_n^{k + n/2}\) 利用性质 \(\omega_n^{2(k+n/2)} = \omega_n^{2k+n} = \omega_n^{2k}\omega_n^n = \omega_{n/2}^k\)\(\omega_n^{n/2} = -1\)\[ \begin{align*} F(\omega_n^{k + n/2}) &= G(\omega_n^{2k + n}) + \omega_n^{k + n/2} \cdot H(\omega_n^{2k+n}) \\ &= G(\omega_{n/2}^k) - \omega_n^k \cdot H(\omega_{n/2}^k) \end{align*} \]

也就是说,只要知道 \(G(\omega_{n/2}^k)\)\(H(\omega_{n/2}^k)\),就能推导出 \(F(\omega_n^k)\)\(F(\omega_n^{k + n/2})\),即:

\[ \begin{cases} F(\omega_n^k) &= G(\omega_{n/2}^k) + \omega_n^k H(\omega_{n/2}^k) \\ F(\omega_n^{k + n/2}) &= G(\omega_{n/2}^k) - \omega_n^k H(\omega_{n/2}^k) \end{cases} \]

注意到分治左右两边长度应当相等,所以长度只能是 \(2\) 的幂,不足要补齐。

另外单位根是复数,由于 STL 提供的 Complex 常数巨大,需要手写。

倍增法实现

假设你轻松的写出了分治法的 FFT,你会发现你 TLE 了。究其原因是递归的常数太大了,我们考虑不用递归。

我们考虑递归到最底层时,系数数组的顺序是怎样的,以 \(8\) 次多项式为例:

  • 第一层:\((x_0,x_1,x_2,x_3,x_4,x_5,x_6,x_7)\)
  • 第二层:\((x_0,x_2,x_4,x_6),(x_1,x_3,x_5,x_7)\)
  • 第三层:\((x_0,x_4),(x_2,x_6),(x_1,x_5),(x_3,x_7)\)
  • 第四层:\((x_0),(x_4),(x_2),(x_6),(x_1),(x_5),(x_3),(x_7)\)

观察其二进制表示:

  • 0 (000) -> 0 (000)
  • 1 (001) -> 4 (100)
  • 2 (010) -> 2 (010)
  • 3 (011) -> 6 (110) 可以发现,最终序列的下标恰好是原序列下标的 二进制位翻转 形式,我们称这个变换为 位逆序置换(bit-reversal permutation)。
简单证明

每一层奇偶拆分,等价于根据当前下标的 最低有效位 (LSB) 进行分组。LSB 为 0 的分到左边,为 1 的分到右边。经过 \(\log n\) 次拆分,原下标的最高位最终决定了它在新序列中的最低位,反之亦然。

补充

这里补充如何快速知道其二进制位的翻转形式,设 \(r_x\)\(x\) 二进制翻转之后的数。

显然,\(r_0 = 0\),我们从小到大求 \(r\)

我们只要把 \(x\) 右移一位,翻转,再右移一位,就能得到 \(x\) 二进制下除了个位的翻转结果。

  • 个位如果是 \(0\),反转后最高位为 \(0\)
  • 个位如果是 \(1\),反转后最高位为 \(1\),因此加上 \(2^{len -1}\)

即:

\[ R(x) =\lfloor \frac{R(\lfloor\frac{x}{2}\rfloor)}{2} \rfloor + (x \bmod 2) \cdot 2^{len -1} \]

容易发现我们可以 \(O(n)\) 获得最后的系数顺序,我们就可以从下往上依次合并,我们还需解决合并问题,回到这个式子:

\[ \begin{cases} F(\omega_n^k) &= G(\omega_{n/2}^k) + \omega_n^k H(\omega_{n/2}^k) \\ F(\omega_n^{k + n/2}) &= G(\omega_{n/2}^k) - \omega_n^k H(\omega_{n/2}^k) \end{cases} \]

观察使用位逆序置换后:

  • \(G(\omega_{n/2}^k)\) 存在数组下标为 \(k\) 的位置,\(H(\omega_{n/2}^k)\) 存在数组下标为 \(k + n/2\) 的位置。

那么我们可以:

  • \(F(\omega_n^k)\) 存在数组下标为 \(k\) 的位置,\(F(\omega_n^{k + n/2})\) 存在数组下标为 \(k + n/2\) 的位置。

这样我们就可以直接覆盖,而不用额外数组,我们称这种方法为 蝶形运算

快速傅里叶逆变换 (IFFT)

快速傅里叶逆变换(Inverse Fast Fourier Transform,IFFT)是 FFT 的逆过程,其目标是将 点值表示法 还原为 系数表示法

从线性代数角度看,DFT 是一个线性变换,可以表示为 \(\mathbf{y} = V \mathbf{a}\),其中 \(\mathbf{a}\) 是系数向量,\(\mathbf{y}\) 是点值向量,\(V\) 是 DFT 的变换矩阵:

\[ V_{jk} = \omega_n^{jk} \quad \text{其中 } \omega_n = e^{i\frac{2\pi}{n}} \]

那么,IFFT 就是找到一个逆矩阵 \(V^{-1}\),使得 \(\mathbf{a} = V^{-1} \mathbf{y}\)

幸运的是,这个 DFT 矩阵的逆矩阵具有一个非常优美的形式。设 \(V^{-1}\) 是一个矩阵,其元素为:

\[ (V^{-1})_{jk} = \frac{1}{n} \omega_n^{-jk} \quad \text{其中 } \omega_n^{-1} = e^{-i\frac{2\pi}{n}} \]

也就是说,逆矩阵 \(V^{-1}\) 只需要将原矩阵 \(V\) 中的单位根 \(\omega_n\) 替换为其共轭复数 \(\omega_n^{-1}\),然后将整个矩阵除以 \(n\) 即可。

证明

基于单位根的一个关键性质,有时被称为 求和引理

\[ \sum_{k=0}^{n-1} (\omega_n^j)^k = \begin{cases} n & \text{if } j \equiv 0 \pmod n \\ 0 & \text{otherwise} \end{cases} \]

发现相乘为单位矩阵。

实现

由于 FFT 和 IFFT 的转移矩阵只有正负不一样,所以我们在编写 FFT 函数时,可以多加形参 type=1/-1 来区分 FFT 和 IFFT,给单位根取不同的符号。

记得 IFFT 最后要除以 \(n\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <algorithm>
#include <cmath>
#include <iostream>
using namespace std;

const int MAXN = 4e6 + 10;
const double Pi = acos(-1.0);

struct Complex {
double x, y;
Complex(double _x = 0, double _y = 0) { x = _x, y = _y; }
} a[MAXN], b[MAXN];

Complex operator+(Complex a, Complex b) { return Complex(a.x + b.x, a.y + b.y); }
Complex operator-(Complex a, Complex b) { return Complex(a.x - b.x, a.y - b.y); }
Complex operator*(Complex a, Complex b) { return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }

int n, m; // 多项式次数
int l, r[MAXN]; // l: 二进制位数, r: 位逆序置换数组
int limit = 1; // limit: 补齐 2 的幂次后的长度

// FFT 主函数
// type = 1: DFT (系数转点值)
// type = -1: IDFT (点值转系数)
void FFT(Complex *A, int type) {
// 进行位逆序置换
for (int i = 0; i < limit; i++)
if (i < r[i]) swap(A[i], A[r[i]]);

// 迭代模拟分治,mid 表示待合并区间的长度的一半
for (int mid = 1; mid < limit; mid <<= 1) {
Complex Wn(cos(Pi / mid), type * sin(Pi / mid));

// R 是区间长度,R = 2 * mid
for (int j = 0; j < limit; j += (mid << 1)) {
Complex w(1, 0);
// 枚举左半部分,进行蝴蝶变换
for (int k = 0; k < mid; k++, w = w * Wn) {
Complex x = A[j + k]; // 左半部分 G(k)
Complex y = w * A[j + mid + k]; // 右半部分 w * H(k)

A[j + k] = x + y;
A[j + mid + k] = x - y;
}
}
}
}

int main() {
cin >> n >> m;

for (int i = 0; i <= n; i++) cin >> a[i].x;
for (int i = 0; i <= m; i++) cin >> b[i].x;

while (limit <= n + m) limit <<= 1, l++;

for (int i = 0; i < limit; i++)
r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));

// FFT 变换
FFT(a, 1);
FFT(b, 1);

// 点值相乘 O(n)
for (int i = 0; i < limit; i++) a[i] = a[i] * b[i];

// IFFT 逆变换
FFT(a, -1);

for (int i = 0; i <= n + m; i++)
cout << (int)(a[i].x / limit + 0.5);

return 0;
}