快速傅里叶变换(FFT)
快速傅里叶变换(FFT)
前言
本文为个人学习笔记,大量参考了 oi-wiki 以及其他博客的内容。
问题
记:
在 内解决两个多项式乘法后的系数(即给定 和 的系数,要你求出 的系数)。
分析
暴力显然是 的,优化的想法是先考虑点值表示,再考虑从点值表示转换为系数表示。
具体如下:
点值表示的意思是,你需要求出( 是什么先忽略,当作是 个已知量即可):
那么:
实际上, 个点的点值表示法也确定了一个 次的多项式,因此,一定存在某个算法能将点值表示法转化为系数表示(这个后面再说)。
至此,FFT 的核心思想已经说清楚了,就是考虑求出 的点值表示,那么 的点值表示就可以在 的复杂度内求出,而后再考虑从点值表示转化为系数表示。
问题一:求出某个多项式的点值表示(离散傅里叶变换 DFT)
实际上这个问题的真实含义是:怎么选取 这个已知量才能使得在一个优秀的复杂度内求出多项式的点值表示。
记 表示将复数坐标系的单位圆平均分成 份,从 轴逆时针出发的第 条分界箭头的复数表示。
选取这个 的原因是它有某些性质,能 " 在一个优秀的复杂度内求出多项式的点值表示 "。
性质:
然后开始推式子:
令
显然有
将 代入有:
将 代入有:
递归求解即可,有 层,时间复杂度为 ,为了方便处理,一般把 处理为 的二次幂,多出来的部分系数补为 即可。
具体实现中有以下注意事项:
1、虚数可以使用 C++ STL 库中的 complex 类型;
代码
#include <bits/stdc++.h>
template < typename T >
inline void read(T &cnt) {
cnt = 0; char ch = getchar(); bool op = 1;
for (; ! isdigit(ch); ch = getchar())
if (ch == '-') op = 0;
for (; isdigit(ch); ch = getchar())
cnt = cnt * 10 + ch - 48;
cnt = op ? cnt : - cnt;
}
const int N = (1 << 22) + 5;
const double PI = acos(-1);
inline void FFT(std::complex < double > *A, int n) {
if (n == 1) return;
int m = (n >> 1);
std::complex < double > A0[m], A1[m];
for (int i = 0; i < m; ++ i) {
A0[i] = A[i * 2];
A1[i] = A[i * 2 + 1];
}
FFT(A0, m); FFT(A1, m); // 递归处理
auto W = std::complex < double > (cos(2.0 * PI / n), sin(2.0 * PI / n)),
w = std::complex < double > (1.0, 0.0); // 从 w_n^0 出发
for (int i = 0; i < m; ++ i) { // 根据式子计算 A 即可
A[i] = A0[i] + w * A1[i];
A[i + m] = A0[i] - w * A1[i];
w *= W; // 等价于 w_n^k -> w_n^{k + 1}
}
}
int n, m;
std::complex < double > F[N], G[N];
int main() {
read(n), read(m);
for (int i = 0; i <= n; ++ i) {
int x; read(x);
F[i] = x;
}
for (int i = 0; i <= m; ++ i) {
int x; read(x);
G[i] = x;
}
int sum = 1;
while (sum <= n + m) sum *= 2; // 补齐成二次幂
FFT(F, sum);
FFT(G, sum);
for (int i = 0; i < sum; ++ i)
F[i] *= G[i];
return 0;
}
优化
递归实在太慢了!
以 项多项式为例,模拟拆分的过程:
- 初始序列为
- 一次二分之后
- 两次二分之后
- 三次二分之后
规律:其实就是原来的那个序列,每个数用二进制表示,然后把二进制翻转对称一下,就是最终那个位置的下标。比如 是 001,翻转是 100,也就是 4,而且最后那个位置确实是 4。我们称这个变换为位逆序置换(bit-reversal permutation),证明留给读者自证。
实际上,位逆序置换可以 从小到大递推实现,设 ,其中 k 表示二进制数的长度,设 表示长度为 的二进制数 翻转后的数(高位补 0)。我们要求的是 。
首先 。
我们从小到大求 。因此在求 时, 的值是已知的。因此我们把 右移一位(除以 ),然后翻转,再右移一位,就得到了 除了(二进制)个位之外其它位的翻转结果。
考虑个位的翻转结果:如果个位是 0,翻转之后最高位就是 0。如果个位是 1,则翻转后最高位是 1,因此还要加上 。综上
举个例子:设 ,。为了翻转 :
- 考虑 ,我们知道 ,再右移一位就得到了 。
- 考虑个位,如果是 ,它就要翻转到数的最高位,即翻转数加上 ,如果是 则不用更改。
蝶形运算优化
已知 和 后,需要使用下面两个式子求出 和 :
使用位逆序置换后,对于给定的 :
- 的值存储在数组下标为 的位置, 的值存储在数组下标为 的位置。
- 的值将存储在数组下标为 的位置, 的值将存储在数组下标为 的位置。
因此可以直接在数组下标为 和 的位置进行覆写,而不用开额外的数组保存值。此方法即称为 蝶形运算,或更准确的,基 - 2 蝶形运算。
再详细说明一下如何借助蝶形运算完成所有段长度为 的合并操作:
1、令段长度为 ;
2、同时枚举序列 的左端点 和序列 的左端点 ;
3、合并两个段时,枚举 ,此时 存储在数组下标为 的位置, 存储在数组下标为 的位置;
4、使用蝶形运算求出 和 ,然后直接在原位置覆写。
代码
#include <bits/stdc++.h>
template < typename T >
inline void read(T &cnt) {
cnt = 0; char ch = getchar(); bool op = 1;
for (; ! isdigit(ch); ch = getchar())
if (ch == '-') op = 0;
for (; isdigit(ch); ch = getchar())
cnt = cnt * 10 + ch - 48;
cnt = op ? cnt : - cnt;
}
const int N = (1 << 22) + 5;
const double PI = acos(-1);
int rev[N];
inline void change(std::complex < double > *A, int n) {
for (int i = 0; i < n; ++ i) { // 求 R 数组
rev[i] = rev[i >> 1] >> 1;
if (i & 1) {
rev[i] |= (n >> 1);
}
}
for (int i = 0; i < n; ++ i) // 将原序列 变为 底层对应的序列
if (i < rev[i]) std::swap(A[i], A[rev[i]]);
}
inline void FFT(std::complex < double > *A, int n) {
change(A, n);
for (int m = 2; m <= n; m *= 2) { // m 是当前处理的每段长度
auto W = std::complex < double >
(cos(2.0 * PI / m), sin(2.0 * PI / m));
for (int x = 0; x < n; x += m) { // x 是每段的开头
auto w = std::complex < double > (1.0, 0.0);
for (int i = x; i < x + m / 2; ++ i) { // 求出每段的点值表示 根据公式求即可
auto A0 = A[i], A1 = A[i + m / 2];
A[i] = A0 + w * A1;
A[i + m / 2] = A0 - w * A1;
w *= W;
}
}
}
}
int n, m;
std::complex < double > F[N], G[N];
int main() {
change(F, 8);
read(n), read(m);
for (int i = 0; i <= n; ++ i) {
int x; read(x);
F[i] = x;
}
for (int i = 0; i <= m; ++ i) {
int x; read(x);
G[i] = x;
}
int sum = 1;
while (sum <= n + m) sum *= 2; // 补齐成二次幂
FFT(F, sum);
FFT(G, sum);
for (int i = 0; i < sum; ++ i)
F[i] *= G[i];
FFT(F, sum);
return 0;
}
问题二:将点值表示转化为系数表示(傅里叶反变换 IDFT)
点值表示的矩阵形式为:
怎么求系数 呢?根据线性代数的知识:
如果能求出 ,那么 也是两个多项式相乘的结果,FFT 即可。
唯一的问题变为怎么求解 。
根据矩阵的逆的定义,有
设 为原矩阵, 为逆矩阵,考虑最终落在 的值:
引理
当 不是 的倍数时,
证明如下:
令 ,则:
当 不为 的倍数(0)时,上式为 0;
反之,有:
再前面补个系数 即可,故:
int main() {
change(F, 8);
read(n), read(m);
for (int i = 0; i <= n; ++ i) {
int x; read(x);
F[i] = x;
}
for (int i = 0; i <= m; ++ i) {
int x; read(x);
G[i] = x;
}
int sum = 1;
while (sum <= n + m) sum *= 2; // 补齐成二次幂
FFT(F, sum);
FFT(G, sum);
for (int i = 0; i < sum; ++ i)
F[i] *= G[i];
FFT(F, sum);
std::reverse(F + 1, F + sum); // 从第一位开始翻转
// 翻转后变为 0 1-n, 2-n, ..., -1
// 实际上等价于 0, 1, 2, ..., n-1
for (int i = 0; i <= n + m; ++ i) { // 四舍五入
std::cout << (int)(F[i].real() / sum + 0.5) << ' ';
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧