集合幂级数学习笔记
Post time: 2022-02-17 14:11:52
本篇文章借鉴于武汉二中吕凯风的2015集训队论文《集合幂级数的性质与应用及其快速算法》。
一、声明
我们令全集为有限集 \(U=\{1,2,...,n\}\),其中 \(n=|U|\),设所有未标明的 \(S\) 都是 \(U\) 的子集。
若 \(X\) 是一个集合,令 \(2^{X}\) 表示 \(X\) 的幂集(所有子集构成的集合)。
二、定义
设 \(F\) 是一个域,则称函数 \(f:2^{U}\to F\) 是 \(F\) 上的一个集合幂级数。对于每个 \(S\in 2^U\),记 \(f_S\) 为 \(S\) 处的函数值,称 \(f_S\) 为集合幂级数 \(S\) 项的系数。
集合幂级数之间的加减法运算是很好定义的,就是对应项的系数加减,可以在 \(O(2^n)\) 的时间内完成。
容易发现一个集合幂级数一定可以写成若干个 \(cx^S\) 相加的形式(其中这个 \(c\) 和 \(x^S\) 之间是某种乘法,但不需要严格定义,只需满足与加法的结合律),所以可以用符号 \(f=\sum_{S\in 2^U}f_Sx^S\) 来表示一个集合幂级数。
乘法的定义复杂一些,首先我们考虑定义的乘法应当对加法有分配律,即
考虑一下这个 $* $ 号运算应当具有的性质:
- \(L* R=R* L\) (交换律)
- \((L* M)* R=L*(M* R)\) (结合律)
- \(S*\varnothing=S\) (单位元为 \(\varnothing\))
那么我们可以定义 \(f_Lx^L* g_Rx^R=(f_Lg_R)x^{L* R}\)。
下面介绍 $* $ 不同的三种常见的集合幂级数乘法。
三、集合并卷积
取 \(L* R=L\cup R\),得到集合并卷积。
定义 \(f\cdot g=h\),其中
计算集合并卷积要使用快速莫比乌斯变换(FMT)。
定义一个集合幂级数 \(f\) 的莫比乌斯变换为
反过来,由容斥原理,定义逆莫比乌斯变换(莫比乌斯反演)为
对上面集合并卷积的式子两边进行莫比乌斯变换,可得
由于 \([L\cup R\subseteq S]=[L\subseteq S][R\subseteq S]\),所以
所以计算集合并卷积的步骤,就是先求出 \(f,g\) 的莫比乌斯变换,对应系数相乘之后,再做一次莫比乌斯反演。
现在考虑如何做莫比乌斯变换。
对莫比乌斯变换的一个理解说的是,有一些形如 \(x\leq y\) 的偏序集,把所有 \(x\) 位置的值累加到 \(y\) 位置上就形成了莫比乌斯变换。
计算上述莫比乌斯变换的方法就是,依次考虑集合中每个元素,没有它的贡献到有它的上面,复杂度 \(O(n2^n)\)。代码大概长这样:
点击查看代码
for(int i=0;i<l;++i){
for(int S=0;S<(1<<l);++S){
if(S&(1<<i)) continue;
add(f[S|(1<<i)],f[S]);
}
}
莫比乌斯反演就是把里边的加号改成减号。
四、对称差卷积(异或卷积)
令 \(L* R=L\oplus R\) 可得对称差卷积:
快速计算需要用到快速沃尔什变换(FWT)。
首先注意到对于一个集合 \(S\) 有
当 \(S\) 不为空集时,对于 \(v\in S\) 把每个 \(T\) 和 \(T\oplus \{v\}\) 放在一起就得到和为 \(0\) 了。
利用这个性质化简上式:
由上式定义沃尔什变换为
可得沃尔什逆变换为
则上面的对称差卷积可以化为
沃尔什变换的求法跟莫比乌斯变换差不多,仍然一位一位考虑集合中的数,对于当前位 \(i\),\(f_S:=f_S+f^{S\cup\{i\}},f_{S\cup\{i\}}:=f_S-f_{S\cup\{i\}}\)。逆变换给每一项乘以 \(\frac{1}{2^n}\) 即可。对特征为 \(2\) 的域不能使用沃尔什变换。复杂度 \(O(n2^n)\)。
五、子集卷积
子集卷积形如
快速算法是枚举 \(S\) 和子集 \(L\),\(R=S/L\),这样做复杂度 \(O(3^n)\)。
可以转化为集合并卷积做
UPDATE:原来写的快速沃尔什变换(FWT)
FWT用来加速计算位运算卷积。
广义上的FWT思路为:
设序列 \(a\) 的变换序列为 \(fwt[a]\),若要计算 \(c=a\oplus b\)(直接算是 \(O(n^2)\)),可构造一个变换 \(a\rightarrow fwt[a]\),使其满足正变换和逆变换时间复杂度均小于 \(O(n^2)\),并且 \(fwt[a]\cdot fwt[b]=fwt[c]\),这里 "\(\cdot\)" 一般指点乘(复杂度小于 \(O(n^2)\))。
经过上边的铺垫,可以进行这样一个过程:\(a\rightarrow fwt[a],b\rightarrow fwt[b],fwt[a]\cdot fwt[b]=fwt[c],fwt[c]\rightarrow c\),这个过程时间复杂度是小于 \(O(n^2)\) 的,达到了加速效果。
在 OI 中,对于 OR,AND,XOR
三种卷积都可以用 FWT 优化到 \(O(n\log n)\) 复杂度。
以或卷积为例:
要计算
直接算是 \(O(n^2)\) 的,考虑使用 FWT:
首先注意到 \(j|i=i,k|i=i\rightarrow (j|k)|i=i\)
设 \(fwt[a]_ i=\sum_{j|i=i}a_j\),需要考虑正变换、逆变换和点乘:
首先考虑点乘:对于 \(\forall i\geq 1\),有
正变换:考虑按位从小到大求,设当前最高位为 \(0,1\) 的答案序列(不考虑这一位)分别为 \(a_0,a_1\),合并之后(考虑这一位)分别为 \(b_0,b_1\),则 \(b_0=a_0,b_1=a_1+a_0\),其中 \(+\) 是序列每一位相加。
通过正变换,不难发现逆变换的式子为 \(b_0=a_0,b_1=a_1-a_0\)。
同理,与的式子就是 \(b_0=a_0+a_1,b_1=a_1\),逆变换 \(b_0=a_0-a_1,b_1=a_1\)。
异或的式子比较复杂。首先定义 \(x\oplus y=popcount(x\&y) \bmod 2\),这里 \(popcount(x)\) 表示 \(x\) 二进制下 \(1\) 的个数。
可以发现,\((j\oplus i)\operatorname{xor}(k\oplus i)=(j\operatorname{xor} k)\oplus i\)。
设 \(fwt[a]_ i=\sum_{j\oplus i=0}a_j-\sum_{j\oplus i=1}a_j\),考虑点乘:
正变换:\(b_0=a_0+a_1,b_1=a_0-a_1\),逆变换:\(b_0=\frac{a_0+a_1}{2},b_1=\frac{a_0-a_1}{2}\)。
代码上,所有的正逆变换都可以合并,注意取模,注意修改的顺序即可。
点击查看代码
#include<iostream>
#include<cstdio>
typedef long long ll;
const int N=(1<<17)+13,mod=998244353;
inline int qpow(int a,int k){int s=1;for(;k;k>>=1,a=(ll)a*a%mod)if(k&1)s=(ll)s*a%mod;return s;}
int n,a[N],b[N],ina[N],inb[N];
inline void clear(){
for(int i=0;i<n;++i) a[i]=ina[i],b[i]=inb[i];
}
inline void OR(int *f,int type=1){
for(int mid=2,k=1;mid<=n;mid<<=1,k<<=1){
for(int i=0;i<n;i+=mid){
for(int j=0;j<k;++j){
f[i+j+k]+=(ll)f[i+j]*type%mod;
f[i+j+k]%=mod;
}
}
}
}
inline void AND(int *f,int type=1){
for(int mid=2,k=1;mid<=n;mid<<=1,k<<=1){
for(int i=0;i<n;i+=mid){
for(int j=0;j<k;++j){
f[i+j]+=(ll)f[i+j+k]*type%mod;
f[i+j]%=mod;
}
}
}
}
inline void XOR(int *f,int type=1){
for(int mid=2,k=1;mid<=n;mid<<=1,k<<=1){
for(int i=0;i<n;i+=mid){
for(int j=0;j<k;++j){
int x=f[i+j],y=f[i+j+k];
f[i+j]=(ll)(x+y)*type%mod,f[i+j+k]=(ll)(x-y+mod)*type%mod;
}
}
}
}
inline void mul(){
for(int i=0;i<n;++i) a[i]=(ll)a[i]*b[i]%mod;
}
inline void print(){
for(int i=0;i<n;++i) printf("%d ",a[i]);
putchar('\n');
}
int main(){
scanf("%d",&n);n=(1<<n);
for(int i=0;i<n;++i) scanf("%d",&ina[i]);
for(int i=0;i<n;++i) scanf("%d",&inb[i]);
clear();OR(a),OR(b),mul(),OR(a,mod-1);print();
clear();AND(a),AND(b),mul(),AND(a,mod-1);print();
clear();XOR(a),XOR(b),mul(),XOR(a,qpow(2,mod-2));print();
return 0;
}