快速傅里叶变换

前置知识:复数.

本文将介绍一种算法,它支持在 O(nlog⁡n)O(n\log n) 的时间内计算两个 nn 次多项式的乘法,比朴素的 O(n2)O(n^2) 算法更高效.由于两个整数的乘法也可以被当作多项式乘法,因此这个算法也可以用来加速大整数的乘法计算.

引入

我们现在引入两个多项式 AA 和 BB:

A=5x2+3x+7B=7x2+2x+1\begin{aligned} A ={}& 5x^2 + 3x + 7 \\ B ={}& 7x^2 + 2x + 1 \\ \end{aligned}

两个多项式相乘的积 C=A×BC = A \times B,我们可以在 O(n2)O(n^2) 的时间复杂度中解得(这里 nn 为 AA 或者 BB 多项式的次数):

C=A×B=35x4+31x3+60x2+17x+7\begin{aligned} C ={}& A \times B \\ ={}& 35x^4 + 31x^3 + 60x^2 + 17x + 7 \end{aligned}

很明显,多项式 CC 的系数 cic_i 满足 ci=∑j=0iajbi−jc_i = \sum_{j = 0}^i a_j b_{i - j}.而对于这种朴素算法而言,计算每一项的时间复杂度都为 O(n)O(n),一共有 O(n)O(n) 项,那么时间复杂度为 O(n2)O(n^2).

能否加速使得它的时间复杂度降低呢?如果使用快速傅里叶变换的话,那么我们可以使得其复杂度降低到 O(nlog⁡n)O(n \log n).

傅里叶变换

傅里叶变换(Fourier Transform)是一种分析信号的方法,它可分析信号的成分,也可用这些成分合成信号.许多波形可作为信号的成分,傅里叶变换用正弦波作为信号的成分.

设 f(t)f(t) 是关于时间 tt 的函数,则傅里叶变换可以检测频率 ω\omega 的周期在 f(t)f(t) 出现的程度:

F(ω)=F[f(t)]=∫−∞∞f(t)e−iωtdtF(\omega)=\mathbb{F}[f(t)]=\int_{-\infty}^{\infty}f(t)\mathrm{e}^{-\mathrm{i}{\omega}t}dt

它的逆变换是

f(t)=F−1[F(ω)]=12π∫−∞∞F(ω)eiωtdωf(t)=\mathbb{F}^{-1}[F(\omega)]=\frac{1}{2\pi}\int_{-\infty}^{\infty}F(\omega)\mathrm{e}^{\mathrm{i}{\omega}t}d\omega

逆变换的形式与正变换非常类似,分母 2π2\pi 恰好是指数函数的周期.

傅里叶变换相当于将时域的函数与周期为 2π2\pi 的复指数函数进行连续的内积.逆变换仍旧为一个内积.

傅里叶变换有相应的卷积定理,可以将时域的卷积转化为频域的乘积,也可以将频域的卷积转化为时域的乘积.

离散傅里叶变换

离散傅里叶变换(Discrete Fourier transform,DFT)是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT(discrete-time Fourier transform)的频域采样.

傅里叶变换是积分形式的连续的函数内积,离散傅里叶变换是求和形式的内积.

设 {xn}n=0N−1\{x_n\}_{n=0}^{N-1} 是某一满足有限性条件的序列,它的离散傅里叶变换(DFT)为:

Xk=∑n=0N−1xne−i2πNknX_k=\sum_{n=0}^{N-1}x_n\mathrm{e}^{-\mathrm{i}\frac{2\pi}{N}kn}

其中 e\mathrm{e} 是自然对数的底数,ii 是虚数单位.通常以符号 F\mathcal {F} 表示这一变换,即

x^=Fx\hat{x}=\mathcal{F}x

类似于积分形式,它的 逆离散傅里叶变换(IDFT)为:

xn=1N∑k=0N−1Xkei2πNknx_n=\frac{1}{N}\sum_{k=0}^{N-1}X_k\mathrm{e}^{\mathrm{i}\frac{2\pi}{N}kn}

可以记为:

x=F−1x^x=\mathcal{F}^{-1}\hat{x}

实际上,DFT 和 IDFT 变换式中和式前面的归一化系数并不重要.在上面的定义中,DFT 和 IDFT 前的系数分别为 11 和 1N\frac {1}{N}.有时我们会将这两个系数都改 1N\frac{1}{{\sqrt{N}}}.

离散傅里叶变换仍旧是时域到频域的变换.由于求和形式的特殊性,可以有其他的解释方法.

如果把序列 xnx_n 看作多项式 f(x)f(x) 的 xnx^n 项系数,则计算得到的 XkX_k 恰好是多项式 f(x)f(x) 代入单位根 e−2πikN\mathrm{e}^{\frac{-2\pi \mathrm{i}k}{N}} 的点值 f(e−2πikN)f(\mathrm{e}^{\frac{-2\pi \mathrm{i}k}{N}}).

这便构成了卷积定理的另一种解释办法,即对多项式进行特殊的求值操作.离散傅里叶变换恰好是多项式在单位根处进行求值.

例如计算:

(n3)+(n7)+(n11)+(n15)+…\dbinom{n}{3}+\dbinom{n}{7}+\dbinom{n}{11}+\dbinom{n}{15}+\ldots

定义函数 f(x)f(x) 为:

f(x)=(1+x)n=(n0)x0+(n1)x1+(n2)x2+(n3)x3+…f(x)={(1+x)}^n=\dbinom{n}{0}x^0+\dbinom{n}{1}x^1+\dbinom{n}{2}x^2+\dbinom{n}{3}x^3+\ldots

然后可以发现,代入四次单位根 f(i)f(\mathrm{i}) 得到这样的序列:

f(i)=(1+i)n=(n0)+(n1)i−(n2)−(n3)i+…f(\mathrm{i})={(1+\mathrm{i})}^n=\dbinom{n}{0}+\dbinom{n}{1}\mathrm{i}-\dbinom{n}{2}-\dbinom{n}{3}\mathrm{i}+\ldots

于是下面的求和恰好可以把其余各项消掉:

f(1)+if(i)−f(−1)−if(−i)=4(n3)+4(n7)+4(n11)+4(n15)+…f(1)+\mathrm{i}f(\mathrm{i})-f(-1)-\mathrm{i}f(-\mathrm{i})=4\dbinom{n}{3}+4\dbinom{n}{7}+4\dbinom{n}{11}+4\dbinom{n}{15}+\ldots

因此这道数学题的答案为:

(n3)+(n7)+(n11)+(n15)+…=2n+i(1+i)n−i(1−i)n4\dbinom{n}{3}+\dbinom{n}{7}+\dbinom{n}{11}+\dbinom{n}{15}+\ldots=\frac{2^n+\mathrm{i}(1+\mathrm{i})^n-\mathrm{i}(1-\mathrm{i})^n}{4}

这道数学题在单位根处求值,恰好构成离散傅里叶变换.

矩阵公式

由于离散傅立叶变换是一个 线性 算子,所以它可以用矩阵乘法来描述.在矩阵表示法中,离散傅立叶变换表示如下:

[X0X1X2⋮XN−1]=[111⋯11αα2⋯αN−11α2α4⋯α2(N−1)⋮⋮⋮⋱⋮1αN−1α2(N−1)⋯α(N−1)(N−1)][x0x1x2⋮xN−1]\begin{bmatrix} X_{0} \\ X_{1} \\ X_{2} \\ \vdots \\ X_{N-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \alpha & \alpha^{2} & \cdots & \alpha^{N-1} \\ 1 & \alpha^{2} & \alpha^{4} & \cdots & \alpha^{2(N-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \alpha^{N-1} & \alpha^{2(N-1)} & \cdots & \alpha^{(N-1)(N-1)} \end{bmatrix} \begin{bmatrix} x_{0} \\ x_{1} \\ x_{2} \\ \vdots \\ x_{N-1} \end{bmatrix}

其中 α=e−i2πN\alpha = \mathrm{e}^{-\mathrm{i}\frac{2\pi}{N}}.

快速傅里叶变换

FFT 是一种高效实现 DFT 的算法,称为快速傅立叶变换(Fast Fourier Transform,FFT).它对傅里叶变换的理论并没有新的发现,但是对于在计算机系统或者说数字系统中应用离散傅立叶变换,可以说是进了一大步.快速数论变换(NTT)是快速傅里叶变换(FFT)在数论基础上的实现.

在 1965 年,Cooley 和 Tukey 发表了快速傅里叶变换算法.事实上 FFT 早在这之前就被发现过了,但是在当时现代计算机并未问世,人们没有意识到 FFT 的重要性.一些调查者认为 FFT 是由 Runge 和 König 在 1924 年发现的.但事实上高斯早在 1805 年就发明了这个算法,但一直没有发表.

分治法实现

FFT 算法的基本思想是分治.就 DFT 来说,它分治地来求当 x=ωnkx=\omega_n^k 的时候 f(x)f(x) 的值.基 - 2 FFT 的分治思想体现在将多项式分为奇次项和偶次项处理.

举个例子,对于一共 88 项的多项式:

f(x)=a0+a1x+a2x2+a3x3+a4x4+a5x5+a6x6+a7x7f(x) = a_0 + a_1x + a_2x^2+a_3x^3+a_4x^4+a_5x^5+a_6x^6+a_7x^7

按照次数的奇偶来分成两组,然后右边提出来一个 xx:

f(x)=(a0+a2x2+a4x4+a6x6)+(a1x+a3x3+a5x5+a7x7)=(a0+a2x2+a4x4+a6x6)+x(a1+a3x2+a5x4+a7x6)\begin{aligned} f(x) &= (a_0+a_2x^2+a_4x^4+a_6x^6) + (a_1x+a_3x^3+a_5x^5+a_7x^7)\\ &= (a_0+a_2x^2+a_4x^4+a_6x^6) + x(a_1+a_3x^2+a_5x^4+a_7x^6) \end{aligned}

分别用奇偶次次项数建立新的函数:

G(x)=a0+a2x+a4x2+a6x3H(x)=a1+a3x+a5x2+a7x3\begin{aligned} G(x) &= a_0+a_2x+a_4x^2+a_6x^3\\ H(x) &= a_1+a_3x+a_5x^2+a_7x^3 \end{aligned}

那么原来的 f(x)f(x) 用新函数表示为:

f(x)=G(x2)+x×H(x2)f(x)=G\left(x^2\right) + x \times H\left(x^2\right)

利用偶数次单位根的性质 ωni=−ωni+n/2\omega^i_n = -\omega^{i + n/2}_n,和 G(x2)G\left(x^2\right) 和 H(x2)H\left(x^2\right) 是偶函数,我们知道在复平面上 ωni\omega^i_n 和 ωni+n/2\omega^{i+n/2}_n 的 G(x2)G(x^2) 的 H(x2)H(x^2) 对应的值相同.得到:

f(ωnk)=G((ωnk)2)+ωnk×H((ωnk)2)=G(ωn2k)+ωnk×H(ωn2k)=G(ωn/2k)+ωnk×H(ωn/2k)\begin{aligned} f(\omega_n^k) &= G((\omega_n^k)^2) + \omega_n^k \times H((\omega_n^k)^2) \\ &= G(\omega_n^{2k}) + \omega_n^k \times H(\omega_n^{2k}) \\ &= G(\omega_{n/2}^k) + \omega_n^k \times H(\omega_{n/2}^k) \end{aligned}

和:

f(ωnk+n/2)=G(ωn2k+n)+ωnk+n/2×H(ωn2k+n)=G(ωn2k)−ωnk×H(ωn2k)=G(ωn/2k)−ωnk×H(ωn/2k)\begin{aligned} f(\omega_n^{k+n/2}) &= G(\omega_n^{2k+n}) + \omega_n^{k+n/2} \times H(\omega_n^{2k+n}) \\ &= G(\omega_n^{2k}) - \omega_n^k \times H(\omega_n^{2k}) \\ &= G(\omega_{n/2}^k) - \omega_n^k \times H(\omega_{n/2}^k) \end{aligned}

因此我们求出了 G(ωn/2k)G(\omega_{n/2}^k) 和 H(ωn/2k)H(\omega_{n/2}^k) 后,就可以同时求出 f(ωnk)f(\omega_n^k) 和 f(ωnk+n/2)f(\omega_n^{k+n/2}).于是对 GG 和 HH 分别递归 DFT 即可.

考虑到分治 DFT 能处理的多项式长度只能是 2m(m∈N∗)2^m(m \in \mathbf{N}^ \ast ),否则在分治的时候左右不一样长,右边就取不到系数了.所以要在第一次 DFT 之前就把序列向上补成长度为 2m(m∈N∗)2^m(m \in \mathbf{N}^\ast )(高次系数补 00)、最高项次数为 2m−12^m-1 的多项式.

在代入值的时候,因为要代入 nn 个不同值,所以我们代入 ωn0,ωn1,ωn2,⋯ ,ωnn−1(n=2m(m∈N∗))\omega_n^0,\omega_n^1,\omega_n^2,\cdots, \omega_n^{n-1} (n=2^m(m \in \mathbf{N}^ \ast )) 一共 2m2^m 个不同值.

代码实现方面,STL 提供了复数的模板,当然也可以手动实现.两者区别在于,使用 STL 的 complex 可以调用 exp 函数求出 ωn\omega_n.但事实上使用欧拉公式得到的虚数来求 ωn\omega_n 也是等价的.

以上就是 FFT 算法中 DFT 的介绍,它将一个多项式从系数表示法变成了点值表示法.

值的注意的是,因为是单位复根,所以说我们需要令 nn 项式的高位补为零,使得 n=2k,k∈N∗n = 2 ^ k, k \in \mathbf{N}^ \ast.

递归版 FFT
#include <cmath>
#include <complex>

using Comp = std::complex<double>;  // STL complex

constexpr Comp I(0, 1);  // i
constexpr int MAX_N = 1 << 20;

Comp tmp[MAX_N];

// rev=1, DFT; rev=-1, IDFT
// 应用完本函数后需要注意归一化系数的处理
void DFT(Comp* f, int n, int rev) {
  if (n == 1) return;
  for (int i = 0; i < n; ++i) tmp[i] = f[i];
  // 偶数放左边,奇数放右边
  for (int i = 0; i < n; ++i) {
    if (i & 1)
      f[n / 2 + i / 2] = tmp[i];
    else
      f[i / 2] = tmp[i];
  }
  Comp *g = f, *h = f + n / 2;
  // 递归 DFT
  DFT(g, n / 2, rev), DFT(h, n / 2, rev);
  // cur 是当前单位复根,对于 k = 0 而言,它对应的单位复根 omega^0_n = 1.
  // step 是两个单位复根的差,即满足 omega^k_n = step*omega^{k-1}*n,
  // 定义等价于 exp(I*(-2*M_PI/n*rev))
  Comp cur(1, 0), step(cos(2 * M_PI / n), sin(-2 * M_PI * rev / n));
  for (int k = 0; k < n / 2;
       ++k) {  // F(omega^k_n) = G(omega^k*{n/2}) + omega^k*n\*H(omega^k*{n/2})
    tmp[k] = g[k] + cur * h[k];
    // F(omega^{k+n/2}*n) = G(omega^k*{n/2}) - omega^k_n*H(omega^k\_{n/2})
    tmp[k + n / 2] = g[k] - cur * h[k];
    cur *= step;
  }
  for (int i = 0; i < n; ++i) f[i] = tmp[i];
}

时间复杂度 O(nlog⁡n)O(n\log n).

倍增法实现

这个算法还可以从「分治」的角度继续优化.对于基 - 2 FFT,我们每一次都会把整个多项式的奇数次项和偶数次项系数分开,一直分到只剩下一个系数.但是,这个递归的过程需要更多的内存.因此,我们可以先「模仿递归」把这些系数在原数组中「拆分」,然后再「倍增」地去合并这些算出来的值.

对于「拆分」,可以使用位逆序置换实现.

对于「合并」,使用蝶形运算优化可以做到只用 O(1)O(1) 的额外空间来完成.

位逆序置换

以 88 项多项式为例,模拟拆分的过程:

  • 初始序列为 {x0,x1,x2,x3,x4,x5,x6,x7}\{x_0, x_1, x_2, x_3, x_4, x_5, x_6, x_7\}
  • 一次二分之后 {x0,x2,x4,x6},{x1,x3,x5,x7}\{x_0, x_2, x_4, x_6\},\{x_1, x_3, x_5, x_7 \}
  • 两次二分之后 {x0,x4}{x2,x6},{x1,x5},{x3,x7}\{x_0,x_4\} \{x_2, x_6\},\{x_1, x_5\},\{x_3, x_7 \}
  • 三次二分之后 {x0}{x4}{x2}{x6}{x1}{x5}{x3}{x7}\{x_0\}\{x_4\}\{x_2\}\{x_6\}\{x_1\}\{x_5\}\{x_3\}\{x_7 \}

规律:其实就是原来的那个序列,每个数用二进制表示,然后把二进制翻转对称一下,就是最终那个位置的下标.比如 x1x_1 是 001,翻转是 100,也就是 4,而且最后那个位置确实是 4.我们称这个变换为位逆序置换(bit-reversal permutation),证明留给读者自证.

根据它的定义,我们可以在 O(n)O(n) 的时间内求出每个数变换后的结果:

位逆序置换实现(O(n)O(n))
/*
 * 进行 FFT 和 IFFT 前的反置变换
 * 位置 i 和 i 的二进制反转后的位置互换
 * len 必须为 2 的幂
 */
void change(Complex y[], int len) {
  // 一开始 i 是 0...01,而 j 是 10...0,在二进制下相反对称.
  // 之后 i 逐渐加一,而 j 依然维持着和 i 相反对称,一直到 i = 1...11.
  for (int i = 1, j = len / 2, k; i < len - 1; i++) {
    // 交换互为小标反转的元素,i < j 保证交换一次
    if (i < j) swap(y[i], y[j]);
    // i 做正常的 + 1,j 做反转类型的 + 1,始终保持 i 和 j 是反转的.
    // 这里 k 代表了 0 出现的最高位.j 先减去高位的全为 1 的数字,
    // 直到遇到了 0,之后再加上即可.
    // 考虑 j 中比特位的翻转次数,最高位将会翻转 n 次,
    // 第二高位将会翻转 n/2 次,以此类推,所以时间复杂度为:
    // T(n) = n + n/2 + n/4 + ... = O(n)
    k = len / 2;
    while (j >= k) {
      j = j - k;
      k = k / 2;
    }
    j += k;
  }
}

位逆序置换也可以 O(n)O(n) 从小到大递推实现,设 len=2klen=2^k,其中 kk 表示二进制数的长度,设 R(x)R(x) 表示长度为 kk 的二进制数 xx 翻转后的数(高位补 00).我们要求的是 R(0),R(1),⋯ ,R(n−1)R(0),R(1),\cdots,R(n-1).

首先 R(0)=0R(0)=0.

我们从小到大求 R(x)R(x).因此在求 R(x)R(x) 时,R(⌊x2⌋)R\left(\left\lfloor \dfrac{x}{2} \right\rfloor\right) 的值是已知的.因此我们把 xx 右移一位(除以 22),然后翻转,再右移一位,就得到了 xx 除了(二进制)个位 之外其它位的翻转结果.

考虑个位的翻转结果:如果个位是 00,翻转之后最高位就是 00.如果个位是 11,则翻转后最高位是 11,因此还要加上 len2=2k−1\dfrac{len}{2}=2^{k-1}.综上

R(x)=⌊R(⌊x2⌋)2⌋+(x mod 2)×len2R(x)=\left\lfloor \frac{R\left(\left\lfloor \frac{x}{2} \right\rfloor\right)}{2} \right\rfloor + (x\bmod 2)\times \frac{len}{2}

举个例子:设 k=5k=5,len=(100000)2len=(100000)_2.为了翻转 (11001)2(11001)_2:

  1. 考虑 (1100)2(1100)_2,我们知道 R((1100)2)=R((01100)2)=(00110)2R((1100)_2)=R((01100)_2)=(00110)_2,再右移一位就得到了 (00011)2(00011)_2.
  2. 考虑个位,如果是 11,它就要翻转到数的最高位,即翻转数加上 (10000)2=2k−1(10000)_2=2^{k-1},如果是 00 则不用更改.
位逆序置换实现(O(n)O(n))
// 同样需要保证 len 是 2 的幂
// 记 rev[i] 为 i 翻转后的值
void change(Complex y[], int len) {
  for (int i = 0; i < len; ++i) {
    rev[i] = rev[i >> 1] >> 1;
    if (i & 1) {  // 如果最后一位是 1,则翻转成 len/2
      rev[i] |= len >> 1;
    }
  }
  for (int i = 0; i < len; ++i) {
    if (i < rev[i]) {  // 保证每对数只翻转一次
      swap(y[i], y[rev[i]]);
    }
  }
  return;
}

蝶形运算优化

已知 G(ωn/2k)G(\omega_{n/2}^k) 和 H(ωn/2k)H(\omega_{n/2}^k) 后,需要使用下面两个式子求出 f(ωnk)f(\omega_n^k) 和 f(ωnk+n/2)f(\omega_n^{k+n/2}):

f(ωnk)=G(ωn/2k)+ωnk×H(ωn/2k)f(ωnk+n/2)=G(ωn/2k)−ωnk×H(ωn/2k)\begin{aligned} f(\omega_n^k) & = G(\omega_{n/2}^k) + \omega_n^k \times H(\omega_{n/2}^k) \\ f(\omega_n^{k+n/2}) & = G(\omega_{n/2}^k) - \omega_n^k \times H(\omega_{n/2}^k) \end{aligned}

使用位逆序置换后,对于给定的 n,kn, k:

  • G(ωn/2k)G(\omega_{n/2}^k) 的值存储在数组下标为 kk 的位置,H(ωn/2k)H(\omega_{n/2}^k) 的值存储在数组下标为 k+n2k + \dfrac{n}{2} 的位置.
  • f(ωnk)f(\omega_n^k) 的值将存储在数组下标为 kk 的位置,f(ωnk+n/2)f(\omega_n^{k+n/2}) 的值将存储在数组下标为 k+n2k + \dfrac{n}{2} 的位置.

因此可以直接在数组下标为 kk 和 k+n2k + \frac{n}{2} 的位置进行覆写,而不用开额外的数组保存值.此方法即称为 蝶形运算,或更准确的,基 - 2 蝶形运算.

再详细说明一下如何借助蝶形运算完成所有段长度为 n2\frac{n}{2} 的合并操作:

  1. 令段长度为 s=n2s = \frac{n}{2};
  2. 同时枚举序列 {G(ωn/2k)}\{G(\omega_{n/2}^k)\} 的左端点 lg=0,2s,4s,⋯ ,N−2sl_g = 0, 2s, 4s, \cdots, N-2s 和序列 {H(ωn/2k)}\{H(\omega_{n/2}^k)\} 的左端点 lh=s,3s,5s,⋯ ,N−sl_h = s, 3s, 5s, \cdots, N-s;
  3. 合并两个段时,枚举 k=0,1,2,⋯ ,s−1k = 0, 1, 2, \cdots, s-1,此时 G(ωn/2k)G(\omega_{n/2}^k) 存储在数组下标为 lg+kl_g + k 的位置,H(ωn/2k)H(\omega_{n/2}^k) 存储在数组下标为 lh+kl_h + k 的位置;
  4. 使用蝶形运算求出 f(ωnk)f(\omega_n^k) 和 f(ωnk+n/2)f(\omega_n^{k+n/2}),然后直接在原位置覆写.

快速傅里叶逆变换

傅里叶逆变换可以用傅里叶变换表示.对此我们有两种理解方式.

线性代数角度

IDFT(傅里叶反变换)的作用,是把目标多项式的点值形式转换成系数形式.而 DFT 本身是个线性变换,可以理解为将目标多项式当作向量,左乘一个矩阵得到变换后的向量,以模拟把单位复根代入多项式的过程:

[y0y1y2y3⋮yn−1]=[1111⋯11ωn1ωn2ωn3⋯ωnn−11ωn2ωn4ωn6⋯ωn2(n−1)1ωn3ωn6ωn9⋯ωn3(n−1)⋮⋮⋮⋮⋱⋮1ωnn−1ωn2(n−1)ωn3(n−1)⋯ωn(n−1)2][a0a1a2a3⋮an−1]\begin{bmatrix}y_0 \\ y_1 \\ y_2 \\ y_3 \\ \vdots \\ y_{n-1} \end{bmatrix} = \begin{bmatrix}1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \cdots & \omega_n^{n-1} \\ 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \cdots & \omega_n^{2(n-1)} \\ 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \cdots & \omega_n^{3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \omega_n^{3(n-1)} & \cdots & \omega_n^{(n-1)^2} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ a_2 \\ a_3 \\ \vdots \\ a_{n-1} \end{bmatrix}

现在我们已经得到最左边的结果了,中间的 xx 值在目标多项式的点值表示中也是一一对应的,所以,根据矩阵的基础知识,我们只要在式子两边左乘中间那个大矩阵的逆矩阵就行了.

由于这个矩阵的元素非常特殊,它的逆矩阵也有特殊的性质,就是每一项 取倒数,再 除以变换的长度 nn,就能得到它的逆矩阵.

注意:傅里叶变换的长度,并不是多项式的长度,变换的长度应比乘积多项式的长度长.待相乘的多项式不够长,需要在高次项处补 00.

为了使计算的结果为原来的倒数,根据欧拉公式,可以得到

1ωk=ωk−1=e−2πik=cos⁡(2πk)+isin⁡(−2πk)\frac{1}{\omega_k}=\omega_k^{-1}=\mathrm{e}^{-\frac{2\pi \mathrm{i}}{k}}=\cos\left(\frac{2\pi}{k}\right)+\mathrm{i} \sin\left(-\frac{2\pi}{k}\right)

因此我们可以尝试着把单位根 ωk\omega_k 取成 e−2πik\mathrm{e}^{-\frac{2\pi \mathrm{i}}{k}},这样我们的计算结果就会变成原来的倒数,之后唯一多的操作就只有再 除以它的长度 nn,而其它的操作过程与 DFT 是完全相同的.我们可以定义一个函数,在里面加一个参数 11 或者是 −1-1,然后把它乘到 π\pi 上.传入 11 就是 DFT,传入 −1-1 就是 IDFT.

单位复根周期性

利用单位复根的周期性同样可以理解 IDFT 与 DFT 之间的关系.

考虑原本的多项式是 f(x)=a0+a1x+a2x2+⋯+an−1xn−1=∑i=0n−1aixif(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}=\sum_{i=0}^{n-1}a_ix^i.而 IDFT 就是把你的点值表示还原为系数表示.

考虑 构造法.我们已知 yi=f(ωni),i∈{0,1,⋯ ,n−1}y_i=f\left( \omega_n^i \right),i\in\{0,1,\cdots,n-1\},求 {a0,a1,⋯ ,an−1}\{a_0,a_1,\cdots,a_{n-1}\}.构造多项式如下

A(x)=∑i=0n−1yixiA(x)=\sum_{i=0}^{n-1}y_ix^i

相当于把 {y0,y1,y2,⋯ ,yn−1}\{y_0,y_1,y_2,\cdots,y_{n-1}\} 当做多项式 AA 的系数表示法.

这时我们有两种推导方式,这对应了两种实现方法.

方法一

设 bi=ωn−ib_i=\omega_n^{-i},则多项式 AA 在 x=b0,b1,⋯ ,bn−1x=b_0,b_1,\cdots,b_{n-1} 处的点值表示法为 {A(b0),A(b1),⋯ ,A(bn−1)}\left\{ A(b_0),A(b_1),\cdots,A(b_{n-1}) \right\}.

对 A(x)A(x) 的定义式做一下变换,可以将 A(bk)A(b_k) 表示为

A(bk)=∑i=0n−1f(ωni)ωn−ik=∑i=0n−1ωn−ik∑j=0n−1aj(ωni)j=∑i=0n−1∑j=0n−1ajωni(j−k)=∑j=0n−1aj∑i=0n−1(ωnj−k)i\begin{aligned} A(b_k)&=\sum_{i=0}^{n-1}f(\omega_n^i)\omega_n^{-ik}=\sum_{i=0}^{n-1}\omega_n^{-ik}\sum_{j=0}^{n-1}a_j(\omega_n^i)^{j}\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j\omega_n^{i(j-k)}=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}\left(\omega_n^{j-k}\right)^i\\ \end{aligned}

记 S(ωna)=∑i=0n−1(ωna)iS\left(\omega_n^a\right)=\sum_{i=0}^{n-1}\left(\omega_n^a\right)^i.

当 a=0(modn)a=0 \pmod{n} 时,S(ωna)=nS\left(\omega_n^a\right)=n.

当 a≠0(modn)a\neq 0 \pmod{n} 时,我们错位相减

S(ωna)=∑i=0n−1(ωna)iωnaS(ωna)=∑i=1n(ωna)iS(ωna)=(ωna)n−(ωna)0ωna−1=0\begin{aligned} S\left(\omega_n^a\right)&=\sum_{i=0}^{n-1}\left(\omega_n^a\right)^i\\ \omega_n^a S\left(\omega_n^a\right)&=\sum_{i=1}^{n}\left(\omega_n^a\right)^i\\ S\left(\omega_n^a\right)&=\frac{\left(\omega_n^a\right)^n-\left(\omega_n^a\right)^0}{\omega_n^a-1}=0\\ \end{aligned}

也就是说

S(ωna)={n,a=00,a≠0S\left(\omega_n^a\right)= \begin{cases} n,&a=0\\ 0,&a\neq 0 \end{cases}

那么代回原式

A(bk)=∑j=0n−1ajS(ωnj−k)=ak⋅nA(b_k)=\sum_{j=0}^{n-1}a_jS\left(\omega_n^{j-k}\right)=a_k\cdot n

也就是说给定点 bi=ωn−ib_i=\omega_n^{-i},则 AA 的点值表示法为

{(b0,A(b0)),(b1,A(b1)),⋯ ,(bn−1,A(bn−1))}={(b0,a0⋅n),(b1,a1⋅n),⋯ ,(bn−1,an−1⋅n)}\begin{aligned} &\left\{ (b_0,A(b_0)),(b_1,A(b_1)),\cdots,(b_{n-1},A(b_{n-1})) \right\}\\ =&\left\{ (b_0,a_0\cdot n),(b_1,a_1\cdot n),\cdots,(b_{n-1},a_{n-1}\cdot n) \right\} \end{aligned}

综上所述,我们取单位根为其倒数,对 {y0,y1,y2,⋯ ,yn−1}\{y_0,y_1,y_2,\cdots,y_{n-1}\} 跑一遍 FFT,然后除以 nn 即可得到 f(x)f(x) 的系数表示.

方法二

我们直接将 ωni\omega_n^i 代入 A(x)A(x).

推导的过程与方法一大同小异,最终我们得到 A(ωnk)=∑j=0n−1ajS(ωnj+k)A(\omega_n^k) = \sum_{j=0}^{n-1}a_jS\left(\omega_n^{j+k}\right).

当且仅当 j+k=0(modn)j+k=0 \pmod{n} 时有 S(ωnj+k)=nS\left(\omega_n^{j+k}\right) = n,否则为 00.因此 A(ωnk)=an−k⋅nA(\omega_n^k) = a_{n-k}\cdot n.

这意味着我们将 {y0,y1,y2,⋯ ,yn−1}\{y_0,y_1,y_2,\cdots,y_{n-1}\} 做 DFT 变换后除以 nn,再反转后 n−1n - 1 个元素,同样可以还原 f(x)f(x) 的系数表示.

代码实现

所以我们 FFT 函数可以集 DFT 和 IDFT 于一身.代码实现如下:

非递归版 FFT(对应方法一)
/*
 * 做 FFT
 * len 必须是 2^k 形式
 * on == 1 时是 DFT,on == -1 时是 IDFT
 */
void fft(Complex y[], int len, int on) {
  // 位逆序置换
  change(y, len);
  // 模拟合并过程,一开始,从长度为一合并到长度为二,一直合并到长度为 len.
  for (int h = 2; h <= len; h <<= 1) {
    // wn:当前单位复根的间隔:w^1_h
    Complex wn(cos(2 * PI / h), sin(on * 2 * PI / h));
    // 合并,共 len / h 次.
    for (int j = 0; j < len; j += h) {
      // 计算当前单位复根,一开始是 1 = w^0_n,之后是以 wn 为间隔递增: w^1_n
      // ...
      Complex w(1, 0);
      for (int k = j; k < j + h / 2; k++) {
        // 左侧部分和右侧是子问题的解
        Complex u = y[k];
        Complex t = w * y[k + h / 2];
        // 这就是把两部分分治的结果加起来
        y[k] = u + t;
        y[k + h / 2] = u - t;
        // 后半个 「step」 中的ω一定和 「前半个」 中的成相反数
        // 「红圈」上的点转一整圈「转回来」,转半圈正好转成相反数
        // 一个数相反数的平方与这个数自身的平方相等
        w = w * wn;
      }
    }
  }
  // 如果是 IDFT,它的逆矩阵的每一个元素不只是原元素取倒数,还要除以长度 len.
  if (on == -1) {
    for (int i = 0; i < len; i++) {
      y[i].x /= len;
      y[i].y /= len;
    }
  }
}
非递归版 FFT(对应方法二)
/*
 * 做 FFT
 * len 必须是 2^k 形式
 * on == 1 时是 DFT,on == -1 时是 IDFT
 */
void fft(Complex y[], int len, int on) {
  change(y, len);
  for (int h = 2; h <= len; h <<= 1) {             // 模拟合并过程
    Complex wn(cos(2 * PI / h), sin(2 * PI / h));  // 计算当前单位复根
    for (int j = 0; j < len; j += h) {
      Complex w(1, 0);  // 计算当前单位复根
      for (int k = j; k < j + h / 2; k++) {
        Complex u = y[k];
        Complex t = w * y[k + h / 2];
        y[k] = u + t;  // 这就是把两部分分治的结果加起来
        y[k + h / 2] = u - t;
        // 后半个 「step」 中的ω一定和 「前半个」 中的成相反数
        // 「红圈」上的点转一整圈「转回来」,转半圈正好转成相反数
        // 一个数相反数的平方与这个数自身的平方相等
        w = w * wn;
      }
    }
  }
  if (on == -1) {
    reverse(y + 1, y + len);
    for (int i = 0; i < len; i++) {
      y[i].x /= len;
      y[i].y /= len;
    }
  }
}
FFT 模板(HDU 1402 - A * B Problem Plus)
#include <cmath>
#include <cstring>
#include <iostream>

const double PI = acos(-1.0);

struct Complex {
  double x, y;

  Complex(double _x = 0.0, double _y = 0.0) {
    x = _x;
    y = _y;
  }

  Complex operator-(const Complex &b) const {
    return Complex(x - b.x, y - b.y);
  }

  Complex operator+(const Complex &b) const {
    return Complex(x + b.x, y + b.y);
  }

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

/*
 * 进行 FFT 和 IFFT 前的反置变换
 * 位置 i 和 i 的二进制反转后的位置互换
 *len 必须为 2 的幂
 */
void change(Complex y[], int len) {
  int i, j, k;

  for (int i = 1, j = len / 2; i < len - 1; i++) {
    if (i < j) std::swap(y[i], y[j]);

    // 交换互为小标反转的元素,i<j 保证交换一次
    // i 做正常的 + 1,j 做反转类型的 + 1,始终保持 i 和 j 是反转的
    k = len / 2;

    while (j >= k) {
      j = j - k;
      k = k / 2;
    }

    if (j < k) j += k;
  }
}

/*
 * 做 FFT
 *len 必须是 2^k 形式
 *on == 1 时是 DFT,on == -1 时是 IDFT
 */
void fft(Complex y[], int len, int on) {
  change(y, len);

  for (int h = 2; h <= len; h <<= 1) {
    Complex wn(cos(2 * PI / h), sin(on * 2 * PI / h));

    for (int j = 0; j < len; j += h) {
      Complex w(1, 0);

      for (int k = j; k < j + h / 2; k++) {
        Complex u = y[k];
        Complex t = w * y[k + h / 2];
        y[k] = u + t;
        y[k + h / 2] = u - t;
        w = w * wn;
      }
    }
  }

  if (on == -1) {
    for (int i = 0; i < len; i++) {
      y[i].x /= len;
    }
  }
}

constexpr int MAXN = 200020;
Complex x1[MAXN], x2[MAXN];
char str1[MAXN / 2], str2[MAXN / 2];
int sum[MAXN];
using std::cin;
using std::cout;

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  while (cin >> str1 >> str2) {
    int len1 = strlen(str1);
    int len2 = strlen(str2);
    int len = 1;

    while (len < len1 * 2 || len < len2 * 2) len <<= 1;

    for (int i = 0; i < len1; i++) x1[i] = Complex(str1[len1 - 1 - i] - '0', 0);

    for (int i = len1; i < len; i++) x1[i] = Complex(0, 0);

    for (int i = 0; i < len2; i++) x2[i] = Complex(str2[len2 - 1 - i] - '0', 0);

    for (int i = len2; i < len; i++) x2[i] = Complex(0, 0);

    fft(x1, len, 1);
    fft(x2, len, 1);

    for (int i = 0; i < len; i++) x1[i] = x1[i] * x2[i];

    fft(x1, len, -1);

    for (int i = 0; i < len; i++) sum[i] = int(x1[i].x + 0.5);

    for (int i = 0; i < len; i++) {
      sum[i + 1] += sum[i] / 10;
      sum[i] %= 10;
    }

    len = len1 + len2 - 1;

    while (sum[len] == 0 && len > 0) len--;

    for (int i = len; i >= 0; i--) cout << char(sum[i] + '0');

    cout << '\n';
  }

  return 0;
}

参考文献

  1. 桃酱的算法笔记.

本文内容来自 OI Wiki《快速傅里叶变换》;作者与修订历史见原文。AIOJ 对排版、站内链接和资源路径做了格式适配。

内容依 CC BY-SA 4.0 与 SATA 许可共享;附加条款以原文声明为准。感谢 OI Wiki 作者与贡献者;请按 SATA 约定前往 OI Wiki 仓库 Star 支持。