FFT 快速傅里叶变换
极难2FFT 快速傅里叶变换:多项式乘法的“超级加速器”
你有没有遇到过这样的问题:两个多项式相乘,比如
,如果用传统方法一个一个乘、再合并同类项,项数多了会累得够呛。如果多项式有十万项,普通方法需要计算一百亿次,电脑都得卡半天。
FFT(快速傅里叶变换)就像给多项式乘法装上了“火箭引擎”,它把计算速度从 O(n²) 提升到 O(n log n),让你瞬间算出结果。它背后的数学魔法是“分治”和“单位根”。
现在,我们就来揭开它的神秘面纱吧!
为什么我们需要 FFT?
回忆一下,两个多项式相乘的朴素做法:
乘积 的每一项系数需要内层循环:
这就像统计你和小伙伴的零花钱组合:你有 3 种面额的硬币,他有 4 种,要算所有组合的总和,得把每种面额都配对一次,运算量是硬币数目的乘积。
生活中类似的例子:假如你每周有 5 元零花钱,你朋友有 4 元零花钱,你们俩合在一起买零食,需要知道每种价格(比如 1元、2元、3元……)的零食能买多少种组合?这其实就是一个卷积问题。当硬币种类非常多时,手工计算慢得让人崩溃。
而 FFT 提供了一条捷径:
- 系数表示 → 点值表示:把多项式看作函数,代入特殊的 x 值(单位根),算出函数值。
- 点值相乘:两个多项式在同一个 x 处的函数值直接相乘,就得到了乘积多项式在这个 x 处的值。
- 点值表示 → 系数表示:再通过逆变换(IFFT)把点值变回系数。
这个过程就像:
- 先用“魔法镜”把一堆零钱(系数)变成它们的“影子”(点值),
- 两个影子直接叠在一起(点乘),
- 再用“反魔法镜”把叠好的影子变回零钱(系数)。
因为点乘是 O(n) 的,而变换用分治只需要 O(n log n),所以整体比 O(n²) 快得多。
FFT 的核心思想:分治 + 单位根
为了快速计算多项式在 n 个点上的值(n 是 2 的幂次,不足就补 0),我们需要利用单位根的对称性,以及分治策略。
什么是单位根?
单位根就是方程 的解,一共有 n 个:。
其中
这些点均匀分布在复平面单位圆上,像时钟的刻度一样。
重要性质(蝶形操作的基础):
- 折半定理:
- 对称性:
分治过程
假设有一个 n 次多项式(n 是 2 的幂):
我们想求它在 n 个单位根处的值。直接代入每个根需要 O(n²),太慢。FFT 的做法是:
按奇偶下标分成两个小多项式:
- 偶数下标部分:
- 奇数下标部分:
那么原多项式可以写成:
例子:
(n=4)
- (偶数下标:0 和 2)
- (奇数下标:1 和 3)
验证:,正确。
现在,我们要求 。利用折半定理,,所以
对于 ( 从 0 到 ):
且 仍然成立,所以
这就是著名的蝶形操作(butterfly operation):只要知道了 和 在 个根上的值,就能同时得到 在成对的两个根上的值,一个加、一个减。
生活比喻:想象你有一排小朋友(多项式系数),你想知道每个小朋友的身高在“魔法尺”(单位根)上的读数。你把小朋友分成奇偶两排,先量出每排一半小朋友的读数,然后用“蝶形公式”快速合成全部小朋友的读数。每次分组规模减半,就像给全校学生做体检,先分班,再分小组,最后轻松汇总。
蝶形操作详解(配图想象)
蝶形操作的图形就像蝴蝶的翅膀:左边两个输入( 和 ),右边两个输出(加和减)。
对于每个 ,我们计算:
- 上半部分:
- 下半部分:
其中 是旋转因子。
递归实现:从顶层多项式开始,不断分成两半,到底层(1项)直接返回,然后逐层用蝶形操作合并。整个递归树深度为 ,每层有 次运算,总复杂度 O(n log n)。
递归 FFT 代码详解(含中文注释)
我们先用递归版本理解原理(实际比赛中一般用非递归迭代版本以避免递归开销,但递归更直观)。
C++ 实现
#include <iostream>
#include <complex>
#include <vector>
#include <cmath>
using namespace std;
using Complex = complex<double>; // 复数类型
const double PI = acos(-1.0); // 圆周率
// 递归FFT,a是系数向量,长度n必须为2的幂
// invert = false 表示 DFT(正变换),true 表示 IDFT(逆变换)
void fft(vector<Complex>& a, bool invert) {
int n = a.size(); // 当前长度
if (n == 1) return; // 只有一个点,递归结束
// 分成奇偶两部分
vector<Complex> a0(n / 2), a1(n / 2);
for (int i = 0; i < n / 2; ++i) {
a0[i] = a[2 * i]; // 偶数下标:a0, a2, a4...
a1[i] = a[2 * i + 1]; // 奇数下标:a1, a3, a5...
}
// 递归计算两个子多项式在 n/2 个单位根上的值
fft(a0, invert);
fft(a1, invert);
// 当前层蝶形操作
double angle = 2 * PI / n * (invert ? -1 : 1); // 正变换角度为正,逆变换为负
Complex w(1, 0); // w = omega_n^0 = 1
Complex wn(cos(angle), sin(angle)); // 步长因子 omega_n^1
for (int i = 0; i < n / 2; ++i) {
Complex t = w * a1[i]; // 乘上旋转因子
a[i] = a0[i] + t; // 上半部分
a[i + n / 2] = a0[i] - t; // 下半部分
w *= wn; // w 更新为下一个 omega_n^k
}
}
// 使用FFT进行两个整数多项式乘法
// a, b 是系数向量(低次到高次)
vector<long long> multiplyPolynomialsFFT(const vector<int>& a, const vector<int>& b) {
// 1. 补成长度,找到大于等于目标长度的2的幂
int n = 1;
while (n < a.size() + b.size() - 1) n <<= 1;
// 2. 复制到复数向量
vector<Complex> fa(n, 0), fb(n, 0);
for (size_t i = 0; i < a.size(); ++i) fa[i] = Complex(a[i], 0);
for (size_t i = 0; i < b.size(); ++i) fb[i] = Complex(b[i], 0);
// 3. DFT:将系数转化为点值
fft(fa, false);
fft(fb, false);
// 4. 点乘:对应位置复数相乘(就是函数值相乘)
for (int i = 0; i < n; ++i) fa[i] *= fb[i];
// 5. IDFT:将点值转回系数
fft(fa, true);
// 6. 除以 n 并四舍五入取整(因为IDFT公式要除以n)
vector<long long> result(n);
for (int i = 0; i < n; ++i) {
result[i] = llround(fa[i].real() / n); // 四舍五入,消除微小浮点误差
}
// 7. 去掉尾部多余的0(如果乘积最高次系数为0)
while (result.size() > 1 && result.back() == 0) result.pop_back();
return result;
}
int main() {
// 例子:A(x) = 1 + 2x + 3x^2,B(x) = 5 + 4x
vector<int> A = {1, 2, 3};
vector<int> B = {5, 4};
auto prod = multiplyPolynomialsFFT(A, B);
cout << "A * B = ";
// 从高次到低次输出
for (int i = prod.size() - 1; i >= 0; --i) {
cout << prod[i] << (i > 0 ? " " : "");
}
cout << endl;
// 输出应该是:12 23 10 5 (对应 12x^3 + 23x^2 + 10x + 5)
return 0;
}
Python 实现
import cmath
import math
def fft(a, invert):
"""
递归FFT
a: 复数列表,长度必须为2的幂
invert: False表示DFT,True表示IDFT
"""
n = len(a)
if n == 1:
return
# 分奇偶
a0 = [a[i] for i in range(0, n, 2)]
a1 = [a[i] for i in range(1, n, 2)]
# 递归
fft(a0, invert)
fft(a1, invert)
angle = 2 * math.pi / n * (-1 if invert else 1)
wn = complex(math.cos(angle), math.sin(angle))
w = complex(1, 0)
for i in range(n // 2):
t = w * a1[i] # 乘旋转因子
a[i] = a0[i] + t
a[i + n // 2] = a0[i] - t
w *= wn
def multiply_polynomials_fft(a, b):
"""使用FFT进行整数多项式乘法"""
# 补成长度(2的幂)
n = 1
while n < len(a) + len(b) - 1:
n <<= 1
# 创建复数向量,后面补0
fa = [complex(x, 0) for x in a] + [complex(0, 0)] * (n - len(a))
fb = [complex(x, 0) for x in b] + [complex(0, 0)] * (n - len(b))
# DFT
fft(fa, False)
fft(fb, False)
# 点乘
for i in range(n):
fa[i] *= fb[i]
# IDFT
fft(fa, True)
# 除以n并四舍五入
result = [round(fa[i].real / n) for i in range(n)]
# 去掉尾部多余0
while len(result) > 1 and result[-1] == 0:
result.pop()
return result
# 测试
A = [1, 2, 3] # 3x^2 + 2x + 1
B = [5, 4] # 4x + 5
prod = multiply_polynomials_fft(A, B)
print("A * B =", prod[::-1]) # 反转显示:高次在前
完整示例:一步一步演示
假设我们要计算 ,。手工卷积:
。
用FFT步骤(n取4,补成2的幂):
- 系数向量:fa = [1,2,0,0],fb = [3,4,0,0]
- 计算DFT:
- 求 ω_4^0=1,ω_4^1=i,ω_4^2=-1,ω_4^3=-i 处的值。
- 递归分治后得到:
fa的DFT结果:? 实际是 [1+2i, 1-2i, 1?]…… 我们直接看最终点乘后的逆变换结果。
- 点乘:fa[i] *= fb[i]
- IDFT后除以4,得到系数 [3,10,8,0],去掉尾部0即 [3,10,8]。
这个例子虽然小,但体现了整个流程。
常见错误与调试技巧
1. 忘记补齐长度为2的幂
FFT要求输入长度必须是2的幂,否则递归时无法平分。
解决方法:用 while 循环找到不小于目标长度的最小2的幂,然后补0。
2. 逆变换忘记除以 n
DFT的逆变换公式中,每个结果要除以 n。很多新手做完IDFT后直接取整,得到错误结果。
解决方法:循环中除以 n(或者在递归最外层统一除以 n)。上面代码是在最后除以 n。
3. 浮点精度问题
复数运算有微小误差,导致四舍五入后得到 ±1 的偏差。
解决方法:使用 llround(C++)或 round(Python)四舍五入。对于整数系数,一般没问题。如果需要绝对精确,可以考虑NTT(数论变换)。
4. 递归栈溢出
当 n 很大(如 2^20 ≈ 100万)时,递归深度为 20,栈空间还够,但递归调用复制向量会消耗大量内存和时间。
解决方法:实际竞赛中用迭代版本(非递归的“位逆序置换”+“蝶形操作循环”),可以避免递归开销。
5. 输出顺序问题
多项式系数通常按低次到高次存储(例如 [1,2,3] 表示 1+2x+3x^2)。
输出时注意顺序,不要搞反。
相关指引:下一步学什么?
学会了FFT,你就掌握了一个强大的武器。但它有两个小缺点:
- 存在浮点误差,不适合需要精确整数的场合(比如求大整数乘积、密码学中的模运算)。
- 需要用到复数,而有些语言没有内置复数库。
NTT(数论变换) 就是解决这两个问题的“FFT整数版”。它把单位根换成模大素数下的原根,所有运算都是整数,准确无误。
另外,FFT还有很多应用:
- 大整数乘法:把大整数看成以10为底的多项式,利用FFT快速乘。
- 卷积与信号处理:图像模糊、音频处理等都用到FFT。
- 生成函数:在组合数学中快速计算组合数。
你可以先从迭代FFT(非递归版本) 开始练习,再进阶到 NTT,甚至尝试实现分治 FFT 来优化多项式求逆等问题。
继续加油,数学的世界还有很多有趣的“加速魔法”等着你!
例题精讲
FFT(快速傅里叶变换)算法的时间复杂度是多少?
标准FFT算法要求输入序列的长度必须是2的幂。
在迭代FFT的实现中,最内层循环计算蝶形运算。假设当前步长为len,旋转因子为w,则核心代码为:
for j in range(0, n, len):
for k in range(len//2):
u = a[j+k]
v = a[j+k+len//2] * ___ # 填空处
a[j+k] = u + v
a[j+k+len//2] = u - v
w *= w_step
其中旋转因子初始值应为:在逆快速傅里叶变换(IFFT)中,为了从频域恢复时域信号,通常需要在变换后对每个点除以什么?
FFT可以用于高效计算两个多项式的乘积。