集合幂级数相关
CHANGE LOG
- 2022.2.28 重构整篇文章。原文章见 位运算卷积,子集卷积与高维前缀和。
NOI 大纲里没有把位运算卷积如 FMT,FWT,子集卷积等知识点单独列出,但高维前缀和(SOSDP)是应用比较广泛的重要算法。
学习上述算法,首先要理解什么是集合幂级数。
1. 集合幂级数
1.1 定义
集合幂级数最初由吕凯风在他的 2015 年集训队论文《集合幂级数的性质与应用及其快速算法》中提出。这个名字听起来非常高级,但实际上不难理解。如果读者能够有一点点形式幂级数的基础知识会更容易理解。
为方便说明,我们引入一些记号:
- 定义
表示 的所有幂集,即 的所有子集组成的集合。 - 定义
为全集 ,其中 ,即 的大小。用数字来表示元素是因为我们并不关心集合中的元素具体是什么,数字只是一个易于理解的符号。 - 若无特殊说明,
均为 的子集。
形式化地,域
换句话说,集合幂级数就是从全集的所有子集(全集的幂集)到某个域
通俗地(不严谨),集合幂级数就是把函数的自变量值类型从一个数
- 对比:形式幂级数由序列导出,而集合幂级数由集合导出。
1.2 表示
有了定义,我们考虑如何表示一个集合幂级数。由于它的定义实在有些抽象,我们需要一个方法清晰地描述集合幂级数
类比形式幂级数,我们可以给每个集合
例如,
- 注意,
是一个整体而非 ,因为 本身没有意义,它是占位符。只有它和它前面的系数作为整体时有意义。 表示集合为 ,而 表示 对应的值为 。 - 下文有些地方会将
看成一个 的二进制数 。这表示 ,或者说 即 在二进制下值为 的位。读者需要在集合 和二进制数 之间建立起快速的对应关系。
1.3 加法与乘法
形式幂级数有加法(对应系数相加)和乘法(加法卷积,相关算法有著名的 FFT 和 NTT),集合幂级数自然也需要定义加法和乘法。
加法比较容易,只要将对应系数相加即可。若集合幂级数
乘法的定义就稍微复杂了一点。考虑集合幂级数
考虑如何定义
- 为满足集合幂级数的乘法对加法的分配律,
和 应当以域 中的乘法相结合(存在域 不满足乘法对加法的分配律吗?至少笔者没见过,而且这不重要 = . =),即 是域 中的乘法。一般即模 意义下的整数域 ( )或有理数域 上的乘法( )。 - 为满足集合幂级数的乘法交换律,
应满足交换律。 - 为满足集合幂级数的乘法结合律,
应满足结合律。 - 当然,
还应该有单位元 。这符合集合之间二元运算关系的基本直觉。
常见的满足上述性质的集合二元运算并不唯一。因此,集合幂级数的乘法类型也并不唯一,常见的有 并卷积,对称差卷积 以及 子集卷积。如果将集合的所有子集用二进制数表示,则它们分别对应了位运算卷积中的 或卷积,异或卷积 和 子集卷积。相关算法将在接下来介绍。
2. 集合并卷积
2.1 定义
令集合之间的二元运算
即下标分别为
最暴力地执行卷积,时间复杂度
2.2 分治解法
根据 位运算在每一位的独立性,我们考虑分治求解卷积。
设
显然,
注意到我们将问题分成了两次不包含
vector <int> or_conv(int n, vector <int> &f, vector <int> &g) {
vector <int> h(1 << n);
if(n == 0) return h[0] = 1ll * f[0] * g[0] % mod, h;
vector <int> a(1 << n - 1), b(1 << n - 1), c(1 << n - 1), d(1 << n - 1);
for(int i = 0; i < 1 << n - 1; i++) {
a[i] = f[i], b[i] = g[i];
c[i] = (f[i] + f[i + (1 << n - 1)]) % mod;
d[i] = (g[i] + g[i + (1 << n - 1)]) % mod;
}
vector <int> l = or_conv(n - 1, a, b), r = or_conv(n - 1, c, d);
for(int i = 0; i < 1 << n - 1; i++) h[i] = l[i];
for(int i = 0; i < 1 << n - 1; i++) h[i + (1 << n - 1)] = (r[i] + mod - l[i]) % mod;
return h;
}
2.3 莫比乌斯变换
上述分治做法已经达到了相当优秀的复杂度,但是由于其涉及到递归和数组复制,导致常数并不理想。对分治做法加以改进,我们得到了不需要递归的快速莫比乌斯变换(FMT)和快速沃尔什变换(FWT,这将在第 4 章提到)。
观察分治做法,可以发现在后半部分,我们相当于先将所有
因此,我们考虑 模拟递归的过程。从大到小 枚举每一位
更进一步地,注意到枚举
- 注:关于上述操作得到该形式的原因见 2.4 小节。关于该形式的正确性,见本小节最后。
对于 集合幂级数
考虑
- FMT 和 FMI 互为逆变换。
快速莫比乌斯变换和快速莫比乌斯反演的具体算法在上文已经进行了讲解。观察式子,可以总结出一般形式:枚举每一位
#include <bits/stdc++.h>
using namespace std;
const int N = 1 << 17;
const int mod = 998244353;
int n, a[N], b[N], A[N], B[N], C[N];
void FMT(int *f, int c) {
for(int i = 1; i < 1 << n; i <<= 1)
for(int j = 0; j < 1 << n; j++)
if(i & j)
f[j] = (f[j] + 1ll * c * f[j ^ i] + mod) % mod;
}
int main() {
cin >> n;
for(int i = 0; i < 1 << n; i++) scanf("%d", &a[i]);
for(int i = 0; i < 1 << n; i++) scanf("%d", &b[i]);
memcpy(A, a, N << 2), memcpy(B, b, N << 2);
FMT(A, 1), FMT(B, 1);
for(int i = 0; i < 1 << n; i++) C[i] = 1ll * A[i] * B[i] % mod;
FMT(C, mod - 1);
for(int i = 0; i < 1 << n; i++) printf("%d ", C[i]);
return 0;
}
接下来我们证明 FMT 进行集合并卷积的正确性。
据定义,
2.4 高维前缀和
我们将说明 FMT 操作为什么能得到一个集合幂级数
回忆二维前缀和的做法,我们利用容斥原理得到递推式
事实上,求解高维前缀和有更一般的做法。即首先枚举每一维,对所有数 仅关于这一维 做前缀和。例如,对于三维前缀和
对于更高的维度同理。这样做高维前缀和的时间复杂度是
关于 FMT,令第
2.5 FMT 的性质
注意到 FMT 本质上是一个形如
因此
- 注:相关线性代数知识见 线性代数学习笔记 第一章。
FMT 还有一个重要的性质,那就是
这使得我们在计算若干集合幂级数的集合并卷积时,可以先全部求莫比乌斯变换,对应位置相乘后再莫比乌斯反演回来,而不用每计算两个集合幂级数的卷积就要莫比乌斯反演一遍。显然,一个集合幂级数 FMI 之后再 FMT,所得结果不变(互逆性)。
注意,这和若干形式幂级数求加法卷积通常使用的分治 + FFT(而非分治 FFT!)不同。这是因为两个
3. 集合对称差卷积
3.1 定义
定义二元集合运算 对称差 表示
令集合之间的二元运算
即下标分别为
最暴力地执行卷积,时间复杂度
3.2 分治解法
类似地,利用位运算在每一维独立的性质,考虑分治。
设
如果直接拆成四个子卷积,时间复杂度仍然是
3.3 沃尔什变换
以下内容均来自于吕凯风的论文。
我们进行一步非常神仙的转化:
注意到
因此,考虑令
同理,根据
因此沃尔什变换和沃尔什逆变换互逆。
考虑如何计算沃尔什(逆)变换。直接枚举子集,时间复杂度
设当前考虑第
- 方便起见,我们将
即 和 的按位与的 的个数的奇偶性记作 。
没听懂?没关系,我们换一种理解方式。在计算
同理,当
因此,我们可以考虑化递归为递推,直接从小到大枚举每一位执行
对于沃尔什逆变换,只需在最后对每一项乘以
- 上述快速沃尔什变换的算法和递归算法本质相同,注意我们令
, 相当于沃尔什变换,而 相当于逆沃尔什变换,并将 的逆元分摊在了所有 层当中。
P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT) 部分代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 1 << 17;
const int mod = 998244353;
int n, a[N], b[N], A[N], B[N], C[N];
void FWT(int *f, int n, int coef) {
for(int k = 1; k < 1 << n; k <<= 1)
for(int i = 0; i < 1 << n; i += k << 1)
for(int j = 0, x, y; j < k; j++)
x = f[i | j], y = f[i | j | k],
f[i | j] = (x + y) % mod, f[i | j | k] = (x - y + mod) % mod;
for(int i = 0; i < 1 << n; i++) f[i] = 1ll * f[i] * coef % mod;
}
int main() {
cin >> n;
for(int i = 0; i < 1 << n; i++) scanf("%d", &a[i]);
for(int i = 0; i < 1 << n; i++) scanf("%d", &b[i]);
memcpy(A, a, N << 2), memcpy(B, b, N << 2);
FWT(A, n, 1), FWT(B, n, 1);
for(int i = 0; i < 1 << n; i++) C[i] = 1ll * A[i] * B[i] % mod;
int inv = mod + 1 >> 1;
for(int i = 1; i < n; i++) inv = 1ll * inv * (mod + 1 >> 1) % mod;
FWT(C, n, inv);
for(int i = 0; i < 1 << n; i++) printf("%d ", C[i]);
cout << endl;
return 0;
}
4. 快速沃尔什变换
本章将从另一个角度详细讲解快速沃尔什变换的原理和推导过程,需要一点点线性代数的知识(相关信息见 线性代数学习笔记 第一章)。内容部分来自于 command_block 的博客 位运算卷积(FWT) & 集合幂级数。
4.1 核心思想
依据 DFT 的思想,我们 化卷积为乘积,考虑对集合幂级数进行 线性变换
且
我们利用位运算的一个非常重要的性质,即 各位独立性,将
- 注意,
可以是 任何位运算符,即按位与,按位或和按位异或。 - 注意,矩阵
需 存在逆元,否则没有逆变换。
考虑得到矩阵
同理,当
4.2 或卷积
当
,因此 ,以及 。这说明 和 均等于 或 ,但不同时等于 ,否则 无逆。 ,因此 ,以及 。这说明 或 ,并且 或 。 ,这和上面一条限制等价。 ,因此 ,以及 。这说明 和 均等于 或 ,但不同时等于 ,否则 无逆。
根据上述限制,我们容易枚举得到所有合法的矩阵
上述两个矩阵对应的逆矩阵分别为
因此,通过 FWT 实现的或卷积和通过 FMT 实现的或卷积 本质相同。
对于与卷积同理,读者可自行推导,这里给出结论:常用的矩阵是
4.3 异或卷积
当
- 对于任意
,均有 。所以 。 ,因此 ,以及 。由于上面一条限制,该限制自然满足。 ,因此 ,以及 。这说明 和 等于 或 ,但不相等,否则 无逆。
综上,矩阵
对第二个矩阵求逆(可以直接手算),得到
5. 子集卷积
5.1 定义
将集合之间的二元运算定义为 不相交集合并,即
它和集合并卷积的唯一区别在于两个集合不能有交。暴力枚举子集,时间复杂度
5.2 快速算法
注意到当
根据性质我们有
将等式两边同时取 FMT,得到
P6097【模板】子集卷积 代码如下。
#include <bits/stdc++.h>
using namespace std;
inline int read() {
int x = 0; char s = getchar();
while(!isdigit(s)) s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return x;
}
inline void print(int x) {
if(x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
const int N = 20, mod = 1e9 + 9;
long long f[N + 1][1 << N], g[N + 1][1 << N], h[1 << N], ans[1 << N];
int n;
template <class T> void FMT(T *f, int op) {
for(int j = 1; j < 1 << n; j <<= 1)
for(int i = 0; i < 1 << n; i++)
if(i & j)
f[i] += op ? f[i ^ j] : -f[i ^ j];
for(int i = 0; i < 1 << n; i++) f[i] = (f[i] % mod + mod) % mod;
}
int main() {
cin >> n;
for(int i = 0; i < 1 << n; i++) f[__builtin_popcount(i)][i] = read();
for(int i = 0; i < 1 << n; i++) g[__builtin_popcount(i)][i] = read();
for(int i = 0; i <= n; i++) FMT(f[i], 1), FMT(g[i], 1);
ans[0] = f[0][0] * g[0][0] % mod;
for(int i = 1; i <= n; i++) {
memset(h, 0, sizeof(h));
for(int j = i; ~j; j--) {
for(int p = 0; p < 1 << n; p++) h[p] += f[j][p] * g[i - j][p];
if(j % 9 == 0) for(int p = 0; p < 1 << n; p++) h[p] %= mod;
}
FMT(h, 0);
for(int p = 0; p < 1 << n; p++) if(__builtin_popcount(p) == i) ans[p] = h[p];
}
for(int p = 0; p < 1 << n; p++) print(ans[p]), putchar(' ');
return 0;
}
6. ln 和 exp
该部分内容过于高深,笔者能力有限(笔者对形式幂级数的 ln 和 exp 都不甚了解),因此挖坑待填。
7. 例题
I. CF1530F Bingo
非正解。考虑补集转化,求没有任何一行 / 列(将对角线看成两个特殊的列)的事件全部发生的概率。设
时间复杂度 long long
换成 int
加上 7s 时限,勉强能过。代码。
II. ABC212H Nim Counting
令
*III. 某联考题 /kk
做
次 的变换,求 。 组询问。
, , 。时间限制 2.5s,空间限制 1GB。
IV. AT4168 [ARC100C] Or Plus Max
ARC 也会出裸题?
记录
*V. P4221 [WC2018]州区划分
首先判断一个点集
记
一个显然的子集卷积形式。
这个
时间复杂度
VI. CF914G Sum the Fibonacci
三合一题。
注意到
VII. P5387 [Cnoi2019]人形演舞
打表求出每个数的 SG 值,发现是
VIII. CC21JAN ORAND
将存在性问题转化为计数问题,直接 FWT 即可。一直 FWT 到答案不再更新为止,复杂度为
8. 参考文章
- FWT 详解 知识点 - neither_nor
- 真正理解快速沃尔什变换/快速莫比乌斯变换(FWT|FMT) - Rockdu
- 「学习笔记」集合幂级数 - Joyemang33
- 2015 吕凯风(vfleaking)集训队论文《集合幂级数的性质与应用及其快速算法》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】