FFT笔记
上次更新是在十月份?我到底摸了多久
卷积
对于两个序列,定义它们的卷积为:
单位根
定义复平面单位圆上的次单位根
易证其有如下的性质:DFT
对于一长度为的序列,定义其离散傅里叶变换(DFT)为:
则有其离散傅里叶逆变换(IDFT)为: (标准定义里面和应该互换,但是不影响)暴力证明:考虑的计算式:
设左边矩阵为,接下来从充分性上证明: 显然当时有,而时,由等比数列求和公式: 因此,得证。上面的DFT有如下意义:
- 将作为一组正交基的系数,计算和向量。(在此意义下逆变换就是将向量进行分解,因此逆变换的意义显然,无需上述证明)
- 将作为以个多项式的系数,计算该多项式在点上的取值。
DFT是线性的,即和的DFT等于DFT的和,且常数倍的DFT是DFT的常数倍。
同时DFT的另一个重要性质被称为卷积性,即
证明:把式子写出来: 显然左边式子展开对应的任意一项都唯一地出现在右式内层求和的展开式中,因此两个式子总是等价的。DFT将卷积的操作转化为的操作,因此可以用于加速卷积。
FFT
因为DFT对应点相乘这一操作本身是的,因此我们只要找到快速的DFT/IDFT算法,就可以避免地计算卷积。FFT就是这样一种基于分治的快速DFT算法,时间复杂度为。
首先假设我们需要计算一个序列的DFT(假设是的整数次幂),我们按照下标的奇偶性将这个序列分为两部分,其中且。那我们有:
首先,注意到:
因此我们稍微变一下: (其实本质上就是DFT的序列其实是可以周期拓展的)这使得我们得以计算时DFT的值。因此,最为朴素的FFT实现就是基于递归不断将进行分治,并通过上式对于分治结果进行合并。时间复杂度自然就是。
然而事实一定没有那么简单。递归写法的FFT常数很大,很大程度上是因为合并本身不是in-place的,需要额外申请空间。更快的in-place FFT实现,Cooley–Tukey算法,是基于这样一个思路的:
假设每次分出来的向左走,向右走,那递归版的FFT就会画出一棵很漂亮的二叉树,其中叶子节点对应的下标和序列本身的下标顺序是有很大差别的。我们可以预先快速计算出叶子节点的顺序,然后直接从叶子开始就地一层一层地合并上去。
那叶子节点的顺序遵循怎样的规律呢?其实我们依据奇偶分组本质上是在依据二进制的末位分组,在这样的思想下,我们脑补一下发现这就是叶子节点的下标把原来序列的下标的二进制翻转一下(低位变高位,高位变低位)(即到叶子节点变成)(可以结合画图理解)。这样的话我们维护一个从高位到低位的二进制加法器就行了。
代码如下:
#include <cstdio>
#include <complex>
#include <cmath>
using namespace std;
typedef complex<double> cplx;
const double PI = 3.1415926535897932384626;
void fft(int len, cplx *x, cplx *X, int dir = 1) {
for (int i = 0, k = 0; i < len; i++) {
X[i] = x[k];
int y = len >> 1;
while (y & k) k ^= y, y >>= 1; // 维护倒序加法器
k |= y;
}
for (int s = 2; s <= len; s <<= 1) {
cplx w_(cos(2 * PI / s), dir * sin(2 * PI / s));
for (int l = 0; l < len; l += s) {
int mid = l + (s >> 1);
cplx w = 1;
for (int i = 0; i < s >> 1; i++) { // 逐层向上合并(一次蝴蝶操作)
cplx t = w * X[mid + i];
X[mid + i] = X[l + i] - t;
X[l + i] = X[l + i] + t;
w *= w_;
}
}
}
}