专栏文章
从零开始的 FFT
算法·理论参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @mioqtsoh
- 此快照首次捕获于
- 2025/12/02 23:39 3 个月前
- 此快照最后确认于
- 2025/12/02 23:39 3 个月前
概述
离散傅里叶变换(Discrete Fourier Transform,缩写为 DFT),是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT 的频域采样。
FFT 是一种高效实现 DFT 的算法,称为快速傅立叶变换(Fast Fourier Transform,FFT)。它对傅里叶变换的理论并没有新的发现,但是对于在计算机系统或者说数字系统中应用离散傅立叶变换,可以说是进了一大步。快速数论变换 (NTT) 是快速傅里叶变换(FFT)在数论基础上的实现。
在 1965 年,Cooley 和 Tukey 发表了快速傅里叶变换算法。事实上 FFT 早在这之前就被发现过了,但是在当时现代计算机并未问世,人们没有意识到 FFT 的重要性。一些调查者认为 FFT 是由 Runge 和 König 在 1924 年发现的。但事实上高斯早在 1805 年就发明了这个算法,但一直没有发表。
作用
Q:FFT 有什么用?
A:在信息学竞赛上主要用于多项式乘法。
前置知识
多项式
先看 百度百科。
定义
几个由数和字母的积组成的代数式之和为多项式。对于 是一个关于 的 次多项式。
表示
系数表示
根据定义,若 可以用一个以 为坐标的 维向量表示,这种表示方法可以称为系数表示。
eg:有 可表示为 。
点值表示
发现,关于 多项式给以看成一个关于 的函数。在初中我们就学过一个 次函数可以用 个过此函数的点表示。
eg:有 可表示为 。证明:待定系数,带入 个点,可解出系数。
运算
如下(,):
多项式计算律同整数。
复数
先看 百度百科。
定义1
定义 ,形如 的数为负数,集合符合为 。其中 为实部, 为虚部。
若 , 两复数实部相同虚部相反则称 、 共轭,、 可分别记为 、。
若 复数到原点的距离为复数的模长,记作 。
辐角为负数与实数轴正方向的夹角。
表示
代数表示
如定义如 。
极坐标表示
使用模长加辐角表示如 。
指数表示
如 。
运算
如下 ,:
复数计算律同整数。
定义2
方程 的解是单位根,记为 (),得 。
eg: 的单位根集合为 。
单位根的性质
折半性质:。
证明:
幂运算性质:。
证明:。
负共轭对称性:。
证明:。
正文
终于学完前置知识,下面切入正题。
引
考虑使用不同于普通的点值表示法,因为点值表示乘法时间复杂度为 ,方法为把每个对应的点相乘。如何求点值?最简单的想法是带入 个点的 坐标算出 坐标,但是时间复杂度退化成了 ,由此一个天才般的算法 FFT 就产生了。
推导
FFT
现在有一个多项式 。考虑对其按 次数奇偶分类得到 。
设 ,,则:。
代入 () 得:。
代入 得:。
有没有发现什么?
这两个式子只有常数项相同,所以我们只要计算第一个式子就可以顺便求出第二个式子的值。
可发现一二两式范围相同、无重叠、覆盖整个求解区间,故可分治。
时间复杂度:。
DFT
转点值表示后还要再把起转化成系数表示。
发现之前的计算就是进行了如下矩阵乘法。
定义:,。
对于 。
当 时:。
当 时:。
带入原公式可得:
对比可以发现, 中的每一项都变成了倒数,故只要把单位根替换成倒数跑 FFT 在除 即可。
code(递归)
C#include <bits/stdc++.h>
#include <complex> // 使用STL复数库替代自定义实现
using namespace std;
// 常量定义
const double PI = acos(-1); // 精确计算圆周率(比硬编码更可靠)
const int N = 1 << 21; // 最大处理长度2^21(约2百万项)
typedef complex < double > Comp; // 复数类型简写
Comp f[N], g[N]; // 存储多项式系数的复数数组
vector < int > rev; // 位逆序置换表
/**
* 快速傅里叶变换(非递归优化版)
* @param a 复数数组指针
* @param n 变换长度(必须为2的幂)
* @param op 变换方向:1=正向变换,-1=逆向变换
*/
void FFT(Comp * a, int n, int op) {
// 第一步:位逆序置换(Cache优化关键)
for (int i = 0; i < n; ++i)
if (i < rev[i])
swap(a[i], a[rev[i]]); // 避免重复交换
// 第二步:分层蝴蝶运算(现代CPU流水线友好)
for (int len = 2; len <= n; len <<= 1) {
Comp wn(cos(2 * PI / len), op * sin(2 * PI / len)); // 当前层的单位根
for (int l = 0; l < n; l += len) { // 分块处理
Comp w(1, 0);
for (int k = l; k < l + len / 2; ++k) { // 蝶形运算
Comp x = a[k], y = w * a[k + len / 2];
a[k] = x + y; // 前半部分
a[k + len / 2] = x - y; // 后半部分(利用共轭对称性)
w *= wn; // 更新旋转因子
}
}
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0); // IO优化
// 输入处理
int n, m;
cin >> n >> m; // 两个多项式的最高次项
for (int i = 0; i <= n; ++i) cin >> f[i]; // 读入多项式A
for (int i = 0; i <= m; ++i) cin >> g[i]; // 读入多项式B
// 计算扩展长度(最近的2的幂)
int lim = 1, l = 0;
while (lim <= n + m) lim <<= 1, ++l; // lim=最终长度,l=二进制位数
// 初始化位逆序表(时空权衡优化)
rev.resize(lim);
for (int i = 0; i < lim; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1)); // 位运算魔术
// 正向FFT(系数->点值)
FFT(f, lim, 1);
FFT(g, lim, 1);
// 点值相乘(O(n)复杂度核心)
for (int i = 0; i < lim; ++i) f[i] *= g[i];
// 逆向FFT(点值->系数)
FFT(f, lim, -1);
// 结果输出(注意精度处理)
for (int i = 0; i <= n + m; ++i)
cout << (int)(fabs(f[i].real()) / lim + 0.5) << " "; // 四舍五入
return 0;
}
优化
递推优化
递归实现的 FFT 常数巨大。所以考虑改成递推版。
转移位置
若要递推自然就需要知道转移到哪里。我们来手模一下 项式拆分过程:
- 原始序列为 。
- 第一次拆分 。
- 第二次拆分 。
- 第三次拆分 。
你发现了什么规律吗?
其实就是原来的那个序列,每个数用二进制表示,然后把二进制翻转对称一下,就是最终那个位置的下标。比如 是 ,翻转是 ,也就是 ,而且在最后的位置确实是 。我们称这个变换为位逆序置换。
证明:对于长度为 的序列,位逆序置换函数 将 位二进制数 映射为其位反转结果 。当 时:,。,,命题成立。假设当 时命题成立:对任意 ,若 ,则 。当 时:设 ,按最低位 划分为 ()或 (),其中 。由归纳假设,( 为 位逆序函数)。左子树()逆序结果为 ,右子树()逆序结果为 。合并后,,即 的位反转结果。故对所有 ,位逆序置换 为 的 位二进制逆序。
由归纳步骤可推导出 的递推关系:
。
等价于位运算形式:
由此得到对应下标,之后向上同递归一样合并就可以了。
code
C#include <bits/stdc++.h>
using namespace std;
typedef complex<double> Comp;
const double PI = acos(-1);
const int N = 1 << 21; // 最大处理长度:2^21(约200万项)
Comp f[N], g[N]; // 存储多项式系数的复数数组
vector<int> rev; // 位逆序置换表
/**
* 快速傅里叶变换(非递归优化版)
* @param a 复数数组指针(输入多项式系数/输出点值)
* @param n 变换长度(必须为2的幂)
* @param op 变换方向:1=正向DFT,-1=逆向IDFT
*/
void FFT(Comp* a, int n, int op) {
// 第一步:位逆序置换(Cache优化:仅交换i < rev[i]的元素)
for (int i = 0; i < n; ++i) {
if (i < rev[i]) {
swap(a[i], a[rev[i]]);
}
}
// 第二步:分层蝴蝶运算(自底向上合并子问题)
for (int len = 2; len <= n; len <<= 1) { // len:当前合并的子序列长度
Comp wn(cos(2 * PI / len), op * sin(2 * PI / len)); // 单位根 w_len^1
for (int l = 0; l < n; l += len) { // l:当前块的起始位置
Comp w(1, 0); // 旋转因子初始化为 w_len^0 = 1
for (int k = l; k < l + len/2; ++k) { // 对块内前半部分元素蝴蝶操作
Comp x = a[k];
Comp y = w * a[k + len/2];
a[k] = x + y; // 前半部分结果
a[k + len/2] = x - y; // 后半部分结果(利用对称性)
w *= wn; // 更新旋转因子:w = w_len^(k+1)
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); // IO优化:关闭同步流,加速输入输出
// 输入多项式A和B的系数
int n, m;
cin >> n >> m;
for (int i = 0; i <= n; ++i) cin >> f[i]; // 读入多项式A(次数n)
for (int i = 0; i <= m; ++i) cin >> g[i]; // 读入多项式B(次数m)
// 计算最小扩展长度(2的幂,需覆盖A*B的最高次n+m)
int lim = 1, bit = 0;
while (lim <= n + m) {
lim <<= 1; // lim = 2^bit,其中bit为二进制位数
bit++;
}
// 初始化位逆序表(递推公式:rev[i] = (rev[i>>1]>>1) | ((i&1) << (bit-1)))
rev.resize(lim);
for (int i = 0; i < lim; ++i) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
}
// 正向FFT:系数表示 → 点值表示
FFT(f, lim, 1);
FFT(g, lim, 1);
// 点值相乘:(A*B)的点值 = A的点值 * B的点值(O(n)复杂度)
for (int i = 0; i < lim; ++i) {
f[i] *= g[i];
}
// 逆向FFT:点值表示 → 系数表示(需除以lim归一化)
FFT(f, lim, -1);
// 输出结果:四舍五入取实部(消除浮点误差)
for (int i = 0; i <= n + m; ++i) {
cout << (int)(fabs(f[i].real()) / lim + 0.5) << " ";
}
return 0;
}
三次变两次优化
这里还有一种把总共执行 次的 FFT 改成 次。
我们可以把第一个多项式放在实部第二个放在虚部,求出平方,把虚部取出除 为答案。
设第一个多项式为 ,第二个为 。 按操作得 。
code
C#include <bits/stdc++.h>
using namespace std;
typedef complex<double> Comp;
const double PI = acos(-1);
const int N = 1 << 22; // 最大支持 2^22 项(约400万)
Comp a[N]; // 合并存储多项式:实部=A(x),虚部=B(x)
vector<int> rev; // 位逆序置换表
/**
* 快速傅里叶变换(非递归版)
* @param n 序列长度(2的幂)
* @param op 1=正向DFT,-1=逆向IDFT
*/
void fft(int n, int op) {
// 位逆序置换(预交换)
for (int i = 0; i < n; ++i) {
if (i < rev[i]) swap(a[i], a[rev[i]]);
}
// 分层蝴蝶运算
for (int len = 2; len <= n; len <<= 1) {
Comp wn(cos(2 * PI / len), op * sin(2 * PI / len));
for (int l = 0; l < n; l += len) {
Comp w(1, 0);
for (int k = l; k < l + len/2; ++k) {
Comp x = a[k], y = w * a[k + len/2];
a[k] = x + y;
a[k + len/2] = x - y;
w *= wn;
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
// 读入多项式A(实部)和B(虚部)
double val; // 临时变量存储输入值
for (int i = 0; i <= n; ++i) {
cin >> val;
a[i] = Comp(val, a[i].imag()); // 修改实部,保留原有虚部(初始为0)
}
for (int i = 0; i <= m; ++i) {
cin >> val;
a[i] = Comp(a[i].real(), val); // 保留原有实部,修改虚部
}
// 计算最小长度(覆盖A*B的最高次n+m)
int lim = 1, bit = 0;
while (lim <= n + m) {
lim <<= 1;
bit++;
}
// 初始化位逆序表
rev.resize(lim);
for (int i = 0; i < lim; ++i) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
}
// 正向FFT:将A和B的系数同时转换为点值
fft(lim, 1);
// 点值相乘:(A + Bi)^2 = (A2 - B2) + 2ABi,虚部/2即为A*B的点值
for (int i = 0; i < lim; ++i) {
a[i] = a[i] * a[i]; // 复数平方
}
// 逆向FFT:将结果转换回系数表示(虚部/2为答案)
fft(lim, -1);
// 输出A*B的系数:虚部/2/lim(四舍五入)
for (int i = 0; i <= n + m; ++i) {
double res = a[i].imag() / 2.0 / lim; // 提取虚部并归一化
cout << (int)(fabs(res) + 0.5) << " ";
}
return 0;
}
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...