FFT 学习笔记
应用
FFT,中文“快速傅里叶变换”,用来加速多项式乘法和卷积,可以将
多项式
系数表示法
一个
也可以用每一项的系数表示
点值表示法
把多项式
那么
点值表示法与系数表示法的互相转化
-
对于一个
次多项式,在已知系数的情况下,将其转化为点值表示法,称为 DFT(离散傅里叶变换)。
对 个 分别代入计算,每次 ,总复杂度是 。 -
对于一个由点值表示法表示的
次多项式,将其转化为系数表示法,称为 IDFT(离散傅里叶逆变换)。
即解一个 元 次的方程组,使用高斯消元是 的,可以用拉格朗日插值做到 。
两种表示法在多项式乘法上的区别
-
对于两个用系数表示法表示的多项式
,需要分别枚举两个多项式的次数再将系数相乘,系数表示法做多项式乘法的复杂度为 。 -
对于两个用点值表示法表示的多项式:
两者相乘的结果记为
,则:时间复杂度
。
那么将系数表示法转化为点值,做乘法后再转换为系数表示法会不会更快呢?在使用 DFT 和 IDFT 的情况下显然是不会的,因为转化是
快速傅里叶变换
复数
复数基础详见高中数学必修二第七章。
复数可以写成
在快速傅里叶变换中,我们考虑更换点值表示法中代入的
即这
故
选择这
-
消去引理:
,有 。证明:
-
折半引理:
且 为偶数,有 。证明:
FFT
考虑将
(由于下面的推导需要用到折半定理,所以将
有多项式:
将
令:
则:
令
接着将
可以观察到
问题就转化为了求
IFFT
通过 FFT,我们已经能在
对于前面的 FFT,其过程可以用矩阵乘法来表示:
所以构造出左边矩阵的逆矩阵即可,左边的矩阵满足
相当于做 FFT 时将
有了 FFT 和 IFFT 就可以写出递归版本的多项式乘法,下面是洛谷 P3803 的代码:
#include <bits/stdc++.h>
using i64 = long long;
using namespace std::complex_literals;
using complex = std::complex<double>;
int n, _m;
std::vector<complex> a, b;
const double pi = acos(-1.0);
void FFT(std::vector<complex> &a, int coef) {
int n = a.size();
if (n == 1)
return ;
std::vector<complex> a1, a2;
for (int i = 0; i < n; ++i) {
(i & 1 ? a2 : a1).push_back(a[i]);
}
FFT(a1, coef);
FFT(a2, coef);
double theta = coef * 2 * pi / n;
complex wn{cos(theta), sin(theta)};
complex w = 1;
for (int k = 0; k < n / 2; ++k, w *= wn) {
a[k] = a1[k] + w * a2[k];
a[k + n / 2] = a1[k] - w * a2[k];
}
}
std::vector<int> multiply(std::vector<int> _a, std::vector<int> _b) {
std::vector<complex> a(_a.begin(), _a.end()), b(_b.begin(), _b.end());
int n = a.size(), _m = b.size(), len = n + _m - 2;
for (n += _m; n != (n & -n); ++n) ;
a.resize(n);
b.resize(n);
FFT(a, 1);
FFT(b, 1);
for (int i = 0; i < n; ++i)
a[i] = a[i] * b[i];
FFT(a, -1);
std::vector<int> c(len + 1);
for (int i = 0; i <= len; ++i) {
c[i] = (int)(a[i].real() / n + 0.5);
}
return c;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
++n, ++m;
std::vector<int> a, b;
while (n--) {
int x;
std::cin >> x;
a.push_back(x);
}
while (m--) {
int x;
std::cin >> x;
b.push_back(x);
}
auto ans = multiply(a, b);
for (auto x : ans)
std::cout << x << ' ';
}
交上去能够通过,但常数较大(最慢的点跑了 1.37s)。考虑优化常数,下面介绍小常数迭代写法。
蝴蝶变换
考虑 FFT 过程中下标的变化:
观察到后序列的下标其实就是原序列下标二进制的翻转,因为每次都把奇数放在右边,偶数放在左边。有了这个性质,我们可以预先把下标放在后序列对应的下标上,然后对区间进行合并即可。
代码:
#include <bits/stdc++.h>
using i64 = long long;
using namespace std::complex_literals;
using complex = std::complex<double>;
const double pi = acos(-1.0);
void FFT(std::vector<complex> &a, int coef, const std::vector<int> &rev) {
int n = a.size();
for (int i = 0; i < n; ++i)
if (i < rev[i])
std::swap(a[i], a[rev[i]]);
for (int k = 1; k < n; k <<= 1) {
double theta = coef * pi / k;
complex wn{cos(theta), sin(theta)};
for (int i = 0; i < n; i += k * 2) {
complex w = 1;
for (int j = 0; j < k; ++j, w *= wn) {
auto x = a[i + j], y = a[i + j + k] * w;
a[i + j] = x + y;
a[i + j + k] = x - y;
}
}
}
}
std::vector<int> multiply(std::vector<int> _a, std::vector<int> _b) {
std::vector<complex> a(_a.begin(), _a.end()), b(_b.begin(), _b.end());
int n = a.size(), _m = b.size(), len = n + _m - 2;
for (n += _m; n != (n & -n); ++n) ;
a.resize(n);
b.resize(n);
int s = std::__lg(n);
std::vector<int> rev(n);
for (int i = 1; i < n; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (s - 1));
FFT(a, 1, rev);
FFT(b, 1, rev);
for (int i = 0; i < n; ++i)
a[i] = a[i] * b[i];
FFT(a, -1, rev);
std::vector<int> c(len + 1);
for (int i = 0; i <= len; ++i) {
c[i] = (int)(a[i].real() / n + 0.5);
}
return c;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
++n, ++m;
std::vector<int> a, b;
while (n--) {
int x;
std::cin >> x;
a.push_back(x);
}
while (m--) {
int x;
std::cin >> x;
b.push_back(x);
}
auto ans = multiply(a, b);
for (auto x : ans)
std::cout << x << ' ';
}
交上去最慢的点跑了 590ms,效率超过递归写法的两倍。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!