FFT——从入门到入土
FFT 是一种可以在 的时间复杂度将多项式转为点值表达的算法。实际上, FFT 只是在求解方法上优化了 DFT(离散傅里叶变换)的过程,并没有提出新的理论。但是其高效的复杂度使得它被广泛使用。
阅读此文章前请先对复数有基本的了解。
定义#
系数表示法#
就是使用一个多项式的系数序列来表达这个多项式。
点值表示法#
一个 次多项式可以由 个点来确定。
设 则
多项式卷积#
定义多项式 ,,它们的乘积为
实际上就是简单的直接相乘
单位复根#
定义#
次单位为满足 的解 ,记作 。
这 个单位根均匀分布在复平面的单位圆上。
自然地, 的辐角为 。因此,有
单位根的性质#
-
等分成 块相当于先分成 块后再将每个块分成 块,边界自然不变,
-
。
指数加 相当于绕原点旋转 。
-
设 ,则
离散傅里叶变换#
我们发现,当两个多项式 同时取 时,得到的点分别为 , 取到的点即为 。
这样做是 的。
对于任意系数多项式转点值,我们可以随意取 的值。
但即使是这样,我们代入还是 的复杂度。
快速傅里叶变换#
傅里叶正变换#
FFT 算法的基本思想是分治。
按时域抽取#
具体的讲,我们将多项式分为奇数项和偶数项。
考虑对于多项式 ,将其拆分成奇数项的多项式和偶数项的多项式 和 ,即为
这样拆分利用了单位根的性质,我们尝试将单位根代入求值:
证明:
由性质二可得
这样,我们就利用单位根,将原式在 的值转化成了两个规模更小的多项式的值,递归处理即可。
由递归式 得到 。
注意到这里我们每次都要对每个系数进行点乘,所以必须将多项式长度拓展到二的整数幂上。
按频域抽取#
从另一个角度,我们直接把原多项式分为前半和后半:
注意到 ,即 ,我们根据 的奇偶性分类,可以得到两个 项的多项式,递归处理即可。
这样,我们将问题规模成功缩小了一半,同样可以达到 的复杂度。
傅里叶逆变换#
将点值表达式转回系数表达式的过程被称作傅里叶逆变换。
考虑 本质上是一个线性变换,我们可以给得到的新矩阵乘上一个逆矩阵,来得到初始矩阵。
观察上面的矩阵,不难看出其逆矩阵 。
递归版代码实现#
void fft(comp *x, int n, int type) { //时域
if (n == 1) return;
comp l[n >> 1], r[n >> 1];
for (int i = 0; i < n; i++) { //按奇偶分类
if (!(i & 1))
l[i >> 1] = x[i];
else
r[i >> 1] = x[i];
}
fft(l, n >> 1, type), fft(r, n >> 1, type);
comp wn1 = comp(cos(2 * pi / n), type * sin(2 * pi / n)), wnk = comp(1, 0);
for (int i = 0; i < (n >> 1); i++, wnk *= wn1;) {
x[i] = l[i] + wnk * r[i];
x[i + (n >> 1)] = l[i] - wnk * r[i]; // 计算左半部分
}
}
这份代码常数巨大,不推荐使用。我们需要一种常数更小的写法。
蝴蝶变换#
我们每次递归的时候都要将系数分成两部分。为什么不可以在计算前就将其计算好呢?
以 时为例:
原本系数 ,变换后系数 。
写成二进制后发现,变换后的下标即为原数写成二进制再翻转后的数。
证明咕着。
for (int i = 0; i < len; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) * (len >> 1));
使用时:
for (int i = 0; i < n; i++) if (i < rev[i]) swap(a[i], a[rev[i]]);
这样就免去了递归的大常数。
非递归代码实现#
void fft(comp *x, int n, int tp) { // 时域
for (int i = 0; i <= n; i++) if (i < rev[i]) swap(x[i], x[rev[i]]);
for (int len = 1; len < n; len <<= 1) {
int sz = len * 2;
comp wn1 = comp(cos(PI / len), sin(PI / len) * tp);
for (int l = 0; l < n; l += sz) {
int r = l + len - 1;
comp wnk = 1;
for (int i = l; i <= r; i++) {
comp a = x[i], b = x[i + len];
x[i] = a + wnk * b, x[i + len] = a - wnk * b;
wnk *= wn1;
}
}
}
}
优化方法#
省略蝴蝶变换#
观察 按频域抽取 时进行的分类,手玩可以写出一个将偶数单位根放在左边,奇数单位根放在右边的算法(即按照 的奇偶性分类,偶数放左边奇数放右边)。
此时做一次蝴蝶变换即可得到按 升幂排序的点值表达。
但是考虑到按时域抽取的 IDFT 在开头同样要做一次蝴蝶变换,而做两次蝴蝶变换可以抵消,因此可以直接省略。
更详细的内容可以自行搜索转置原理。
三次变两次#
不知道有啥用,咕。
原根#
呃呃
快速数论变换#
呃呃
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现