FFT(快速傅里叶变换)学习笔记

FFT(快速傅里叶变换)学习笔记

时间:2019.5.8

\(FFT\)(快速傅里叶变换)是一种用于处理多项式的快速算法

多项式乘法:给定一个 \(n\) 次多项式 \(F(x)\),和一个 \(m\) 次多项式 \(G(x)\)
请求出 \(F(x)\)\(G(x)\) 的卷积。

卷积是啥?能吃吗 qwq?

看完这篇文章,你就会明白如何用 \(FFT\) 解决这道题了。不过,在正式讲解前,我先问大家两个问题:

一个数字,它的 \(n\) 次方是 \(1\),请问它是什么?

我们知道,一个 \(2\) 次的方程可能会有 \(2\) 个解,\(3\) 次的方程可能会有 \(3\) 个解……那么这个问题会有 \(n\) 个解吗?

多项式

\(FFT\) 所指的 “多项式” 一般都是只有一元的多项式(即只有 \(x\) 一个字母),形如:

\[F(x) = a _ 0 + a _ 1 x + a _ 2 x ^ 2 + \cdots + a _ {n - 1} x ^ {n - 1} \\ \text {或 } F(x) = \displaystyle \sum _ {i = 0} ^ {n - 1} a _ i x ^ i \]

这是一个 \(n - 1\)\(n\) 项式。下文的多项式也是指这样的式子。

乘法

多项式的乘法,在数学上称为 “卷积”

举个例子吧。

\[\begin {align} \text {随便两个多项式 } F(x) &= 1 + x; \\ G(x) &= 2 + 3x. \\ \end {align} \\ \text {根据初中数学我们可以知道,} (1 + x) \times (2 + 3x) = 1 (2 + 3x) + x (2 + 3x) = 2 + 5x + 3x ^ 2. \\ \text {如果有一个多项式 } H(x) = 2 + 5x + 3x ^ 2 \\ \text {那么我们就说 } F * G = H \text { ,即 } H \text { 是 } F \text { 和 } G \text { 的卷积 } \]

所以说,其实卷积就是多项式普通地相乘起来。

如果要让你计算两个多项式相乘的结果(还是一个多项式),你会怎么做?

我会暴力!ovo

// 让 a * b = c
// a[i] 表示 x^i 项的系数,b、c 同理
k = n + m // 结果多项式的次数,刚好等于两个多项式次数的总和
for (int i = 0; i <= k; i++) {
  c[i] = 0;
  for (int j = 0; j <= n; j++) {
    c[i] += a[j] * b[i - j]; // 嗯?
  }
}

不要吐槽 2 格缩进QAQ感到奇怪?为什么让 c[i] += a[j] * b[i - j] 呢?我们不妨手推一下样例试试(高能预警):

\[\begin {align} F(x) & = 1 + x; \\ G(x) & = 2 + 3x; \\ H(x) & = (F * G) \ (x) \\ & = (1 + x) \times (2 + 3x) \\ & = 1 \times (2 + 3x) + x \times (2 + 3x) \\ & = 1 \times 2 + 1 \times 3x + x \times 2 + x \times 3x \\ & = (1 \times 2) + (1 \times 3 + 1 \times 2) x + (1 \times 3) x \end {align} \]

我们将 \(F\)\(G\)\(H\) 的系数拿出来看看?

\[f: \{ 1, 1 \} \\ g: \{ 2, 3 \} \\ h: \{ 1 \times 2, 1 \times 3 + 1 \times 2, 1 \times 3 \} \\ \text {或者说} \\ h: \{ f _ 0 \times g _ 0, f _ 0 \times g _ 1 + f _ 1 \times g _ 0, f _ 1 \times g _ 1 \} \\ \]

好吧……可能不是很明显,但是 \(H\) 的第 \(i\) 项系数的确等于 \(\displaystyle \sum _ {j + k = i} f _ j g _ k\)。想想多项式乘在一起的过程吧。现在能够明白代码的意思了吗?

但,但暴力的复杂度明显是 \(\mathrm {O} (N ^ 2)\) 的啊……有没有更快的算法呢?

圆与复数

一个数字,它的 \(n\) 次方是 \(1\),请问它是什么?

你会说:它是 1。嗯,当 \(n\) 是偶数时,还可能是 -1,还有吗…好像没了。

听说过 \(i\) 吗?它也许能给我们一些启示。

i 的故事

定义 \(i ^ 2 = -1\)

咦,不可能啊?实数的平方不是非负吗?没关系,这个 \(i\) 不是实数,实数的法则管不了它。

大家也许听说,\(i\) 的名字叫 虚数

复数

实数和虚数的组合叫做复数。

比如 \(2 + 3i\) ,一次乘法,还有一次加法。

\(1 + 2 i ^ 2 + 3 i ^ 3 + 4 i ^ 4\)?不存在的。别忘了 \(i ^ 2 = -1\)

真是奇怪的数……一个复数一般用形如 \(a + bi\) 的形式表示。正如实数对应着数轴上的点一样,每一个复数与平面上的一个点 \((a, b)\) 对应。这个平面被称为“复平面”。图中 \(2 + 3i\) 就对应着点 \((2, 3)\)

复数的 模长 指它的长度(到原点的距离),辐角 指它与原点的连线,与 \(x\) 轴(逆时针)的夹角。

代码实现?用结构体存储复数,分别存下实部和虚部就行了。注意要用 double,以后会用到浮点数。

struct Complex {
  double r, i; // 实部和虚部。r 和 i 分别是英文中“实部”和“虚部”的首字母。
  // 以下是构造函数,看不懂不要紧。
  Complex() : r(0), i(0) {}
  Complex(double r, double i) : r(r), i(i) {}
  Complex(const Complex& c) : r(c.r), i(c.i) {}
};

运算 - 加减法

既然是数,那肯定也有运算吧!复数既有加减法,又有乘除法。
\(FFT\) 中用不到复数的除法,我们这里不讲。)

让我们试一试,将两个复数相加减会怎样?如果有两个复数分别是\(a _ 0 + b _ 0 i\)\(a _ 1 + b _ 1 i\),加减得到:

\((a _ 0 + b _ 0 i) + (a _ 1 + b _ 1 i) = a _ 0 + b _ 0 i + a _ 1 + b _ 1 i = (a _ 0 + a _ 1) + (b _ 0 + b _ 1) i\)。嗯,实部虚部分别相加。

\((a _ 0 + b _ 0 i) - (a _ 1 + b _ 1 i) = a _ 0 + b _ 0 i - a _ 1 - b _ 1 i = (a _ 0 - a _ 1) + (b _ 0 - b _ 1) i\)。嗯,实部虚部分别相减。

(不要怕,简单的拆括号和合并同类项而已)

诶?我们说复数对应平面上的点,那么复数加减有什么几何意义吗?

如图,有两个复数 \(2 + 3i\)\(3 + 1i\)

观察它们相加的结果,\((2 + 3i) + (3 + 1i) = 5 + 4i\)

没错。它们相加的结果可以看成在 \(3 + 1i\) 的端点处向上数 \(3\) 格,向右数 \(2\) 格得到的点。或者说是两个复数所组成平行四边形的一条对角线。(理解一下这句话)

减法呢?\(a - b = a + (-b)\) 嘛。\((-b)\) 就是复数 \(b\) 关于原点的对称点。画个图试试看!

学过向量的同学可以发现:复数加减法的规则和向量加减法是一样的。

因此我们也可以把复数看成是向量,而非点。

代码:

// 重载运算
inline Complex operator+(const Complex& a, const Complex& b) {
  return Complex(a.r + b.r, a.i + b.i);
}
inline Complex operator-(const Complex& a, const Complex& b) {
  return Complex(a.r - b.r, a.i - b.i);
}

运算 - 乘法

乘法呢?好像麻烦一点

\( (a _ 0 + b _ 0 i) \times (a _ 1 + b _ 1 i) \\ = a _ 0 \times (a _ 1 + b _ 1 i) + b _ 0 i \times (a _ 1 + b _ 1 i) \\ = a _ 0 \times a _ 1 + a _ 0 \times b _ 1 i + b _ 0 i \times a _ 1 + b _ 0 i \times b _ 1 i \\ = a _ 0 a _ 1 + a _ 0 b _ 1 i + b _ 0 a _ 1 i - b _ 0 b _ 1 \)

别忘了 \(i ^ 2 = -1\),接下来合并一下同类项。

\(= (a _ 0 a _ 1 - b _ 0 b _ 1) + (a _ 0 b _ 1 + b _ 0 a _ 1) i\)

于是我们就得到了乘法的公式:

inline Complex operator*(const Complex& a, const Complex& b) {
  return Complex(a.r * b.r - a.i * b.i, a.r * b.i + a.i * b.r);
}

复数乘法也有几何意义。模长相乘,辐角相加。怎么推出来的呢?留给大家去思考。

给大家一点提示吧:

可以推出 \((a _ 0 + b _ 0 i) \times (a _ 1 + b _ 1 i) = a _ 0 \times (a _ 1 + b _ 1 i) + b _ 0 \times (-b _ 1 + a _ 1 i)\)。点 \((a _ 1, b _ 1)\) 与原点的连线和点 \((-b _ 1, a _ 1)\) 与原点的连线是什么关系呢?

posted @ 2019-05-10 13:43  longlongzhu123  阅读(286)  评论(0)    收藏  举报