快速傅里叶变换、数论变换、快速莫比乌斯变换、快速沃尔什变换学习笔记(代码还没补,以及应用)
快速傅里叶变换、数论变换、快速莫比乌斯变换、快速沃尔什变换学习笔记
前置知识
阅读下文需要复数和任意角三角函数等高中数学知识,请先自行学习这些内容。
快速傅里叶变换(FFT)
概述
离散傅里叶变换(Discrete Fourier Transform,DFT),是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT 的频域采样。快速傅里叶变换(Fast Fourier Transform,FFT)是一种高效实现 DFT 的算法。
DFT 在算法竞赛中常用于加速多项式乘法,并进一步加速多项式其他各种运算来解题。
多项式的系数与点值表示法
系数表示法是用每一项系数来表示多项式。。
点值表示法是用 次多项式在 个点的取值表示多项式。容易证明 个点的取值可以唯一确定一个不超过 次的多项式。。
两个系数表示法的多项式相乘需要计算 次,而两个点值表示法的多项式相乘只需要在 的时间把对应点值相乘即可,于是只要可以在 的时间实现系数和点值的互转,就可以加速多项式乘法。
离散傅里叶变换(DFT)的作用是将多项式从系数表示法转化为点值表示法,离散傅里叶逆变换(IDFT)的作用是将多项式从点值表示法转化为系数表示法。
了解了 DFT 和 IDFT 的想法,我们发现点值显然不能随便代入,因为这样计算就是 的了,于是我们希望取一些特殊点的点值。
单位复根
我们想到 的幂都很好算,就不需要计算 ()了。同理,我们发现 的幂也很好算,或者说 中的 也可以。
我们定义方程 在复数意义下的解是 次复根,显然这样的解有 个。我们设 ,于是 的解集为 。我们称 为 次单位复根,它是把单位元 等分的第一个角对应的向量。
单位复根有三个重要的性质:
- 。
- 。
- 。
离散傅里叶变换(DFT)
DFT 的思想是分治地求 时 的值,分治的方法是分奇偶次项处理。
注意 DFT 时多项式的次数必须是 ,即有 项,不足的需要补 。
例如对于七次八项式:
进行如下变形:
设函数 :
则:
代入单位复根:
因此如果我们知道 和 ,就可以求出 和 ,可以对 递归进行 DFT。
时间复杂度为 。
【参考代码先咕着】
位逆序置换
DFT 的分治是递归形式的,时间和空间的常数都较大,FFT 就是通过一些神奇的方式把递归改为递推,使得常数变为原来的一半。
我们对刚刚的七次八项式进行模拟递归:
- 。
- 。
- 。
- 。
那么序列 有没有什么特点呢?是有的,把它们写为三位的二进制数,发现就是把二进制高低位翻转了。这一点可以很简单地通过每次考虑最低位的排列方式来证明。这种序列我们称之为位逆序置换,也称蝴蝶变换。
根据位逆序置换的定义,我们显然可以在 的时间求出 的位逆序置换。
事实上位逆序置换也可以早 的时间求出。我们记 的位逆序置换为 ,则可以发现:
【参考代码先咕着】
快速傅里叶变换(FFT)
上面说了,FFT 就是 DFT 改成递推版本,我们先对原多项式进行位逆序置换,然后利用倍增合并的方式处理即可。
【参考代码先咕着】
快速傅里叶逆变换(IFFT)
离散傅里叶逆变换(IDFT)
IDFT 可以用 DFT 表示,有两种理解方式。
理解方式一:线性代数
DFT 的过程可以视为将系数序列视为一个列向量,再左乘一个矩阵得到点值列向量:
我们有了点值向量(也就是左侧的列向量),要求系数向量(也就是右侧的列向量),只需要左乘中间矩阵的逆矩阵即可。
中间矩阵十分特殊,它满足:
根据单位复根的性质及欧拉公式:
我们只需要在 DFT 的基础上把单位根 取成这个数,再把计算结果除以 即可。
理解方式二:单位复根周期性
构造法
原多项式为 ,我们已知 (),要求 。
构造多项式如下:
接下来有两种推导方式,对应两种实现。
推导方式一
设 ,则多项式 在 处的点值为 。
其中,我们可以把 表示为:
我们记 。
当 时,。
当 时,进行错位相减:
综上,我们知道:
代回原式:
所以我们取单位根为其倒数跑 DFT,再除以 得到的就是原多项式的系数表示。
推导方式二
我们将 代入 ,与推导方式一类似,最终得到 ,所以 。
所以我们直接对点值做 DFT 后除以 ,再反转后 个元素,得到的也是原多项式的系数表示。
FFT/IFFT 代码实现
代码实现有两种,如下:
【参考代码先咕着】
【参考代码先咕着】
例题
这个我还得多做点,先咕着。
数论变换(NTT)
概述
NTT 是 FFT 在数论基础上的实现,用来解决带模数的多项式乘法。NTT 的运算过程中没有复数和浮点数,常数较小且没有精度误差。
NTT 的详细讲解需要较大量的群论知识,在下文我尝试避开群论知识把 NTT 讲明白。
原根
对于质数 ,若满足 ,那么它的原根 满足 。
我们记 ,发现 与 有相似的性质。对于正整数 和整数 ,有:(下文 表示对应关系而非推出)
- 。
- 。
- 。
- 。
NTT 模数
常见的 NTT 模数 及其一个原根 有:
- 。
- 。
- 。
数论逆变换(INTT)
类似于 IDFT 的推导过程,不过似乎只能用第二种代码实现。
NTT/INTT 代码实现
【参考代码先咕着】
例题
这个我还得多做点,先咕着。
高维前缀和与快速莫比乌斯变换(FMT)
高维前缀和
我们从二维前缀和开始考虑。
二维前缀和有一种很常用的基于容斥的求法:
// 数组下标从 1 开始
for(int i = 1; i <= k; ++i)
for(int j = 1; j <= k; ++j)
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + a[i][j];
但这种方法不好推广到更高维,原因是容斥的项数是 的(其中 为维数),总复杂度就是 。
二维前缀和还有另一种求法,是一维一维求:
// 数组下标从 1 开始
for(int i = 1; i <= k; ++i)
for(int j = 1; j <= k; ++j)
a[i][j] += a[i - 1][j];
for(int i = 1; i <= k; ++i)
for(int j = 1; j <= k; ++j)
a[i][j] += a[i][j - 1];
这种方法是可以推广的。
具体地,对于一个长度为 的数组(即: 维,每个维度长度为 ),求它的高维前缀和的方法也是一维一维求:
// 注意:为了方便,与前文不同的是,这里数组下标从 0 开始
// let len := pow(k, n)
for(int j = 0, cur = 1; j < n; ++j, cur *= k)
for(int i = 0; i < len; ++i)
if((i / cur) % k)
a[i] += a[i - cur];
复杂度为 ,可以看到比基于容斥的方法要小很多。正确性的话,可以想象前缀和的求和关系为一张有向无环的网格图,我们在上面按照拓扑序进行 DP,因此是正确的。
一般情况下,每个维度的长度为 ,高维前缀和也就是子集关系:
// 数组下标从 0 开始
for(int j = 0; j < n; ++j)
for(int i = 0; i < (1 << n); ++i)
if((i >> j) & 1)
a[i] += a[i ^ (1 << j)];
同理,我们还可以写出高维后缀和,也就是超集关系:
// 数组下标从 0 开始
for(int j = 0; j < n; ++j)
for(int i = 0; i < (1 << n); ++i)
if(!((i >> j) & 1))
a[i] += a[i ^ (1 << j)];
Dirichlet 前缀和
狄利克雷前缀和:。
一种简单的做法是枚举 ,枚举 的倍数 ,然后加上去统计答案,这是调和级数的 。
设 ,我们可以把每个质因数 视为一维,做高维前缀和,复杂度与埃拉托色尼筛一样为 。
代码:(里面求高维前缀和的函数名叫 fmt,不确定这算不算但不重要)
// Problem: P5495 Dirichlet 前缀和
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5495
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
//By: OIer rui_er
#include <bits/stdc++.h>
#define rep(x,y,z) for(uint x=(y);x<=(z);x++)
#define per(x,y,z) for(uint x=(y);x>=(z);x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef unsigned int uint;
const uint N = 2e7+5;
uint n, seed, a[N], tab[N], p[N], pcnt, ans;
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
uint getnext() {
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return seed;
}
void sieve(uint lim) {
rep(i, 2, lim) {
if(!tab[i]) p[++pcnt] = i;
for(uint j=1;j<=pcnt&&i*p[j]<=lim;j++) {
tab[i*p[j]] = 1;
if(!(i % p[j])) break;
}
}
}
void fmt(uint* a, uint n) {
sieve(n);
rep(i, 1, pcnt) for(uint j=1;j*p[i]<=n;j++) a[j*p[i]] += a[j];
}
int main() {
scanf("%u%u", &n, &seed);
rep(i, 1, n) a[i] = getnext();
fmt(a, n);
rep(i, 1, n) ans ^= a[i];
printf("%u\n", ans);
return 0;
}
概述
FMT 常用于处理子集相关的卷积问题。因为我们可以对集合进行状态压缩,所以 FMT 可以处理按位与、按位或等的卷积。
也就是对于序列 ,求序列 ,满足:
其中 为 (交集、按位与)、(并集、按位或)等。
并集卷积(或卷积)
考虑 FFT 的过程,先将 转化为可以直接对应位置相乘的点值表示,再转化回我们想要的系数表示,其中转化的过程用时为 低于暴力的 。
那么我们能不能将 快速转化为可以直接对应位置相乘的序列 ,求出 再快速转化回去呢?
的过程被称为快速莫比乌斯变换(FMT), 的过程被称为快速莫比乌斯逆变换(IFMT)或快速莫比乌斯反演(FMI)。
我们令 ,则根据 ,进行如下推导:
因此上面的 可以直接对应位置相乘,符合我们的要求。问题转化为如何 以及 。
先考虑 。
如果我们暴力枚举 定义式 中的 ,再枚举 的子集 ,全集大小为 ,所以枚举全局的子集的子集的复杂度为 ,显然不符合我们的要求。
这时候可能会想到, 不就是 的高维前缀和嘛!于是我们可以 求解。
再考虑 。
我们知道子集反演公式:
于是:
其中 表示 集合的大小,状态压缩为二进制后也就是 。
如果你对容斥系数比较敏感的话,会想到把高维前缀和改为“高维前缀差”(瞎起的名字),也就是每次累加贡献改为累加负贡献。
【参考代码先咕着】
交集卷积(与卷积)
类似上面的并集卷积(或卷积),我们构造 即可,也就是把 谁是子集谁是超集调换了一下。
代码实现方法和证明方法均同理。
【参考代码先咕着】
卷积和 卷积
也就是 和 。
可以分别构造 和 ,不再赘述。
【参考代码先咕着】
快速沃尔什变换(FWT)
概述
FWT 的思想与 FFT、FMT 相同,也是构造序列 的变换 使得可以对应位置相乘。在算法竞赛中,FWT 常用于处理位运算卷积。
也就是对于序列 ,求序列 ,满足:
其中 为 (交集、按位与)、(并集、按位或)等。
并集卷积(或卷积)
我们令 ,可以用上文 FMT 中的方法证明可以直接对应位置相乘。
先考虑 。
令 为 的前半段和后半段,也就是 是 中下标最高位为 的, 是下标最高位为 的。
则有:
其中 是拼接, 是序列对应位置相加,例如 ,。
于是我们可以分治来解决,而且可以不用递归。
再考虑 。
由上式可得:
同样分治解决,而且两者还可以合在一起写。
【参考代码先咕着】
交集卷积(与卷积)
类似上面的并集卷积(或卷积),相信大家可以自己推出来了!
【参考代码先咕着】
对称差卷积(异或卷积)
对称差的定义:。
这应该是算法竞赛中 FWT 最常考的应用(虽说我刚学还没咋见过)。
定义 ,用状态压缩表示的话就是 。
构造 ,则有:
因此 ,。
【参考代码先咕着】
再记录一个 进制 FWT 的 博客。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下