畜中牲都不一定能理解的 FFT
前言
看了一上午的 FFT 竟然学会了。于是写下这篇来纪念。
期间涉及复平面的相关知识,我这个畜中牲竟然懂了,真是神奇,请不要望而却步,勇于面对,死磕一下总是好的。
FFT 中文名 快速傅里叶变换
OIer 经常拿它来解决高精度乘法的问题。朴素高精乘是
以下保证
What is FFT ?
快速傅里叶变换(FFT)是一种能在
什么是多项式的系数表示法和点值表示法
一个
次的多项式 ,这个就是系数表示法。 而一个
次的多项式,代入 个不同的 值,将会得到 个对应的 值。那么如果我们知道这些数对 ,就可以计算得这个多项式中的每个 。所以这些数对就是点值表示法。
朴素傅里叶变换(DFT,FFT 的理论基础)
强大的傅里叶前辈发明了一种方法,将
牛逼的 C++ 给了复数模板:
头文件 #include <complex>
定义 complex<double> x;
令
于是可以得到单位根的几个性质:
显然,因为表示的都是一个数(实际上你可以带进去计算一下); 因为关于原点对称。 ,显然符合复数相乘的幅角相加法则。
为什么要选择这些来代入式子呢?
这就牵扯到 DFT 的优美性质了。
我们将
- 当
时,原式 ; - 当
时,原式 (等比序列求和)
结论
将
快速傅里叶变换(FFT)
虽然我们搞出了伟大的 DFT 的结论,但如果暴力代入还是
于是 FFT 油然而生。
数学证明
设
先将
设两个多项式:
假设
那么对于
如果我们知道
#include <bits/stdc++.h> using cpd = std::complex<double>; #define rep(i, a, b) for(int i = (a); i <= (b); ++i) #define il inline const int N = 1e6 + 10; const double pi = acos(-1.0); il cpd omega(const int n, const int k) { return cpd(cos(2.0 * k * pi / n), sin(2.0 * k * pi / n)); } il void FFT(cpd *a, int n, bool inv) {//inv 的作用是标记是否要取倒数 if (n == 1) return; static cpd tmp[N]; int m = n / 2; rep(i, 0, m - 1)//按照奇偶分为两组 { tmp[i] = a[2 * i]; tmp[i + m] = a[2 * i + 1]; } rep(i, 0, n - 1) a[i] = tmp[i]; FFT(a, m, inv);//递归处理两个子问题 FFT(a + m, m, inv); rep(i, 0, m - 1) { cpd x = omega(n, i); if(inv) x = conj(x); tmp[i] = a[i] + x * a[i + m]; tmp[i + m] = a[i] - x * a[i + m]; } rep(i, 0, n - 1) a[i] = tmp[i]; } cpd a[N]; int main() { int n; std::cin >> n; rep(i, 0, n - 1) { double t; std::cin >> t; a[i].real(t); } FFT(a, n, true); rep(i, 0, n - 1) std::cout << a[i] << '\n'; }
递归真的超级慢,也超级难写。于是我们学一些优化。
FFT 优化
from 递归 to 非递归
在进行 FFT 时,我们要把各个系数不断分组并放到两侧,那么一个系数原来的位置和最终的位置有什么规律呢?
初始位置 | 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 |
可以发现,这你都能发现,原本位置为 a 的数,最后所在的位置是 a 二进制翻转得到的数,如
那么我们可以据此写出非递归版本 FFT :先把每个数放到最后的位置上,然后不断向上还原,同时求出点值表示。
#include <bits/stdc++.h> using cpd = std::complex<double>; #define rep(i, a, b) for(int i = (a); i <= (b); ++i) #define il inline const int N = 1e6 + 10; const double pi = acos(-1.0); int n; cpd a[N], b[N], omg[N], inv[N]; il void init() { rep(i, 0, n - 1) { omg[i] = cpd(cos(2 * i * pi / n), sin(2 * i * pi / n)); inv[i] = conj(omg[i]); } } il void FFT(cpd *a, cpd *omg) { int lim = 0; while((1 << lim) < n) lim++; rep(i, 0, n - 1) { int t = 0; rep(j, 0, lim - 1) if((i >> j) & 1) t |= (1 << (lim - j - 1)); if(i < t) swap(a[i], a[t]); // i < t 的限制使得每对点只被交换一次(否则交换两次相当于没交换) } static cpd buf[N]; for(int l = 2; l <= n; l *= 2) { int m = l / 2; for(int j = 0; j < n; j += l) rep(i, 0, m - 1) { buf[j + i] = a[j + i] + omg[n / l * i] * a[j + i + m]; buf[j + i + m] = a[j + i] - omg[n / l * i] * a[j + i + m]; } rep(j, 0, n - 1) a[j] = buf[j]; } } int main() { std::cin >> n; init(); rep(i, 0, n - 1) { double t; std::cin >> t; a[i].real(t); } FFT(a, inv); rep(i, 0, n - 1) { std::cout << a[i] << '\n'; } }
蝴蝶优化
别跑呀,实际上很简单。
buf[j + i] = a[j + i] + omg[n / l * i] * a[j + i + m]; buf[j + i + m] = a[j + i] - omg[n / l * i] * a[j + i + m];
我们发现 buf 数组的作用实际上是为了让这两行不互相影响,但是如果写成:
cpd t = omg[n / l * i] * a[j + i + m] a[j + i + m] = a[j + i] - t a[j + i] = a[j + i] + t
就可以抛弃 buf 数组了。
最终版本
#include <bits/stdc++.h> using cpd = std::complex<double>; #define rep(i, a, b) for(int i = (a); i <= (b); ++i) #define il inline const int N = 1e6 + 10; const double pi = acos(-1.0); int n; cpd a[N], b[N], omg[N], inv[N]; il void init() { rep(i, 0, n - 1) { omg[i] = cpd(cos(2 * i * pi / n), sin(2 * i * pi / n)); inv[i] = conj(omg[i]); } } void FFT(cpd *a, cpd *omg) { int lim = 0; while((1 << lim) < n) lim++; for(int i = 0; i < n; i++) { int t = 0; for(int j = 0; j < lim; j++) if((i >> j) & 1) t |= (1 << (lim - j - 1)); if(i < t) swap(a[i], a[t]); // i < t 的限制使得每对点只被交换一次(否则交换两次相当于没交换) } for(int l = 2; l <= n; l *= 2) { int m = l / 2; for(cpd *p = a; p != a + n; p += l) for(int i = 0; i < m; i++) { cpd t = omg[n / l * i] * p[i + m]; p[i + m] = p[i] - t; p[i] += t; } } } int main() { std::cin >> n; init(); rep(i, 0, n - 1) { double t; std::cin >> t; a[i].real(t); } FFT(a, inv); rep(i, 0, n - 1) { std::cout << a[i] << '\n'; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理