q
FFT(Fast Fourier Transform,快速傅里叶变换) 是计算机科学中最伟大的算法之一。简单来说,它的核心作用是:在 \(O(N \log N)\) 的时间内,完成多项式乘法。
如果没有 FFT,我们做多项式乘积需要 \(O(N^2)\)(就像小学生列竖式乘法一样),当项数达到 10 万级别时,普通计算机就跑不动了。
1. 核心思想:从“系数”到“点值”
理解 FFT 的关键在于多项式的两种表示方法:
- 系数表示法:\(A(x) = a_0 + a_1x + a_2x^2 + \dots + a_nx^n\)。这是我们最熟悉的。
- 点值表示法:一个 \(n\) 次多项式可以用 \(n+1\) 个不同的点 \((x_i, y_i)\) 唯一确定。
FFT 的奇妙旅程:
- DFT (FFT): 把两个多项式的系数转成“点值”。
- 点值乘法: 两个多项式在相同 \(x\) 处的 \(y\) 值直接相乘(只需 \(O(N)\))。
- IDFT (IFFT): 把乘完后的点值再转回“系数”。
2. 为什么 FFT 这么快?(单位复根的妙用)
如果随机选 \(x\) 点计算点值,复杂度还是 \(O(N^2)\)。FFT 的天才之处在于选了一组特殊的 \(x\)——单位复根 (\(\omega_n^k\))。
这些点在复平面上均匀分布在一个单位圆上。它们具有对称性和周期性:
- 折半定理:\(\omega_{2n}^{2k} = \omega_n^k\)
- 消去定理:\(\omega_n^{k + n/2} = -\omega_n^k\)
利用这些性质,我们可以把一个规模为 \(N\) 的问题,拆分成两个规模为 \(N/2\) 的子问题(奇数项一组,偶数项一组)。这就是分治法。
3. 递归实现 vs 迭代实现
在你之前的代码里使用的是递归实现。它的逻辑非常清晰:
- 分类:把多项式按下标分为偶数项 \(A_0(x)\) 和奇数项 \(A_1(x)\)。
- 分治:递归计算这两个小多项式在 \(\omega_{n/2}^k\) 处的点值。
- 合并(蝴蝶操作):利用公式合并结果:
- \(A(\omega_n^k) = A_0(\omega_{n/2}^k) + \omega_n^k A_1(\omega_{n/2}^k)\)
- \(A(\omega_n^{k+n/2}) = A_0(\omega_{n/2}^k) - \omega_n^k A_1(\omega_{n/2}^k)\)
注意:递归版虽然好理解,但在 C 语言中由于频繁开辟内存和函数入栈,效率较低。工业界和竞赛通常使用迭代版(非递归)。
4. 形象理解:蝴蝶变换
在迭代版 FFT 中,我们需要先对原始数组进行“位反转置换”(Bit-reversal permutation)。比如下标为 \((1)_{10} = (001)_2\),反转后变成 \((100)_2 = (4)_{10}\)。
这样做之后,我们就可以自底向上地通过“蝴蝶操作”直接在原数组上修改,而不需要额外的临时数组。
5. FFT 的应用场景
FFT 不仅仅是为了解数学题,它在现代技术中无处不在:
- 音频处理:提取音乐的频谱(比如播放器上的频谱跳动)。
- 图像压缩:JPEG 压缩背后有类似的离散余弦变换(DCT)。
- 信号通信:4G/5G 信号调制。
- 大整数乘法:当两个数字有几百万位时,就把它们看作多项式用 FFT 算。
你想深入了解一下如何把你的 C 语言递归代码改写成速度更快的“迭代版(蝴蝶变换)”吗?
既然要“细说”,我们就把 FFT 的外壳剥开。FFT 的本质是利用分治法(Divide and Conquer),而支撑这个分治法能够成立的数学地基,是单位复根的特殊性质。
1. 为什么一定要用复数?
如果我们选普通的实数(如 \(1, 2, 3 \dots\))代入多项式 \(A(x) = \sum a_i x^i\),计算 \(N\) 个点依然需要 \(O(N^2)\)。
但如果我们选 单位圆上的 \(N\) 等分点,奇迹就发生了。
单位复根 \(\omega_n^k\)
在复平面上,以原点为圆心,1 为半径作圆。将圆周 \(N\) 等分,从 \((1,0)\) 开始逆时针数,第 \(k\) 个点就是 \(\omega_n^k\)。
根据欧拉公式,它的坐标是:
这些点有两个救命的性质:
- 折半性:\(\omega_{2n}^{2k} = \omega_n^k\)(在圆上转 2 圈等分点重合)。
- 对称性:\(\omega_n^{k + n/2} = -\omega_n^k\)(相差半圈的点,坐标互为相反数)。
2. 核心推导:奇偶分治
假设我们要计算 \(N\) 项多项式 \(A(x)\)(\(N\) 为 2 的幂)。我们将系数按下标的奇偶性分成两半:
- 偶数项:\(A_0(x) = a_0 + a_2x + a_4x^2 + \dots\)
- 奇数项:\(A_1(x) = a_1 + a_3x + a_5x^2 + \dots\)
那么原多项式可以写成:
当我们把 \(\omega_n^k\)(其中 \(k < n/2\))代入时:
- 前一半 (\(k\)):\(A(\omega_n^k) = A_0(\omega_{n/2}^k) + \omega_n^k A_1(\omega_{n/2}^k)\)
- 后一半 (\(k + n/2\)):\(A(\omega_n^{k+n/2}) = A_0(\omega_{n/2}^k) - \omega_n^k A_1(\omega_{n/2}^k)\)
注意到了吗? 计算后一半的值时,居然完全用的是前一半算过的数据(只是符号变了)!这就是为什么复杂度能从 \(N^2\) 降到 \(N \log N\)。
3. 蝴蝶操作 (Butterfly Operation)
在代码实现中,这种“前一半加、后一半减”的操作被称为蝴蝶操作。
- 左边:是上一层递归传回来的两个点值 \(A_0\) 和 \(A_1\)。
- 右边:是合成出来的当前层更大的多项式点值。
- 这个形状像蝴蝶的翅膀,因此得名。
4. 迭代优化:位反转置换 (Bit-Reversal)
递归版 FFT 有个致命伤:递归太深、频繁申请内存。为了写出更快的迭代版,我们需要观察递归时系数是怎么拆分的。
以 \(N=8\) 为例,拆分过程如下:
- 初始:
0, 1, 2, 3, 4, 5, 6, 7 - 第一层(偶奇):
0, 2, 4, 6|1, 3, 5, 7 - 第二层(再偶奇):
0, 4|2, 6||1, 5|3, 7 - 最终底层:
0, 4, 2, 6, 1, 5, 3, 7
神奇的规律:
把最终底层的下标转为二进制:
0(000), 4(100), 2(010), 6(110), 1(001), 5(101), 3(011), 7(111)
你会发现,它们恰好是原始下标 000, 001, 010, 011... 的 二进制翻转!
所以,迭代版 FFT 的步骤是:
- 打乱:先把数组按二进制翻转重新排列。
- 合并:从底层开始,两两合并,再四四合并……直到合成长度为 \(N\)。
总结:FFT 的三步走
- 补位:把多项式长度补到 \(2^k\)。
- 变换 (DFT):把 \(A\) 和 \(B\) 从“系数”变成“点值”。
- 乘法:对应点值直接相乘 \(C_i = A_i \times B_i\)。
- 逆变换 (IDFT):把 \(C\) 变回“系数”(只需在 FFT 算法中把 \(\omega\) 取共轭复数,最后结果除以 \(N\) 即可)。
你想看一眼这个更高效、不带 malloc 的“迭代版蝴蝶变换”代码吗?
gemini生成的

浙公网安备 33010602011771号