cunzai_zsy0531

关注我

集合幂级数学习笔记

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\) 来表示一个集合幂级数。

乘法的定义复杂一些,首先我们考虑定义的乘法应当对加法有分配律,即

\[\sum_{S\in 2^U}h_Sx^S=(\sum_{L\in 2^U}f_Lx^L)\cdot(\sum_{R\in 2^U}g_Rx^R)=\sum_{L\in 2^U}\sum_{R\in 2^U}(f_Lx^L* g_Rx^R) \]

考虑一下这个 $* $ 号运算应当具有的性质:

  1. \(L* R=R* L\) (交换律)
  2. \((L* M)* R=L*(M* R)\) (结合律)
  3. \(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\),其中

\[h_S=\sum_{L\in 2^U}\sum_{R\in 2^U}[L\cup R=S]f_Lg_R \]

计算集合并卷积要使用快速莫比乌斯变换(FMT)。

定义一个集合幂级数 \(f\) 的莫比乌斯变换为

\[\hat{f}_ S=\sum_{T\subseteq S} f_T \]

反过来,由容斥原理,定义逆莫比乌斯变换(莫比乌斯反演)为

\[f_S=\sum_{T\subseteq S}(-1)^{|S|-|T|}\hat f_T \]

对上面集合并卷积的式子两边进行莫比乌斯变换,可得

\[\hat h_S=\sum_{L\in 2^U}\sum_{R\in 2^U}[L\cup R\subseteq S]f_Lg_R \]

由于 \([L\cup R\subseteq S]=[L\subseteq S][R\subseteq S]\),所以

\[\hat h_S=(\sum_{L\subseteq S}f_L)(\sum_{R\subseteq S}g_R)=\hat f_S\hat g_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\) 可得对称差卷积:

\[h_S=\sum_{L\in 2^U}\sum_{R\in 2^U}[L\oplus R=S]f_Lg_R \]

快速计算需要用到快速沃尔什变换(FWT)。

首先注意到对于一个集合 \(S\)

\[\frac{1}{2^n}\sum_{T\in 2^U}(-1)^{|S\cap T|}=[S=\varnothing] \]

\(S\) 不为空集时,对于 \(v\in S\) 把每个 \(T\)\(T\oplus \{v\}\) 放在一起就得到和为 \(0\) 了。

利用这个性质化简上式:

\[\begin{aligned} h_S&=\sum_{L\in 2^U}\sum_{R\in 2^U}[L\oplus R\oplus S=\varnothing]f_Lg_R\\ &=\sum_{L\in 2^U}\sum_{R\in 2^U}\frac{1}{2^n}\big(\sum_{T\in 2^U}(-1)^{|T\cap (L\oplus R\oplus S)|}\big)f_Lg_R\\ &=\sum_{L\in 2^U}\sum_{R\in 2^U}\frac{1}{2^n}\big(\sum_{T\in 2^U}(-1)^{|T\cap L|}(-1)^{|T\cap R|}(-1)^{|T\cap S|}\big)f_Lg_R\\ &=\frac{1}{2^n}\sum_{T\in 2^U}(-1)^{|T\cap S|}\big(\sum_{L\in 2^U}(-1)^{|T\cap L|}f_L\big)\big(\sum_{R\in 2^U}(-1)^{|T\cap R|}g_R\big) \end{aligned} \]

由上式定义沃尔什变换为

\[\hat f_S=\sum_{T\in 2^U}f_T(-1)^{|T\cap S|} \]

可得沃尔什逆变换为

\[f_S=\frac{1}{2^n}\sum_{T\in 2^U}\hat f_T(-1)^{|T\cap S|} \]

则上面的对称差卷积可以化为

\[\hat h_S=\hat f_S\hat g_S \]

沃尔什变换的求法跟莫比乌斯变换差不多,仍然一位一位考虑集合中的数,对于当前位 \(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)\)

五、子集卷积

子集卷积形如

\[h_S=\sum_{L\in 2^U}\sum_{R\in 2^U}[L\cup R=S][L\cap R=\varnothing]f_Lg_R \]

快速算法是枚举 \(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)\) 复杂度。

以或卷积为例:

要计算

\[c_i=\sum_{j|k=i}a_jb_k \]

直接算是 \(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\),有

\[\begin{aligned} fwt[a]_ i\cdot fwt[b]_ i&= (\sum_{j|i=i}a_j)(\sum_{k|i=i}b_k)\\ &= \sum_{j|i=i}\sum_{k|i=i}a_jb_k\\ &= \sum_{(j|k)|i=i}a_jb_k\\ &= \sum_{(j|k)|i=i}c_{j|k}\\ &= fwt[c]_ i \end{aligned} \]

正变换:考虑按位从小到大求,设当前最高位为 \(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\),考虑点乘:

\[\begin{aligned} fwt[a]_ i\cdot fwt[b]_ i&= (\sum_{j\oplus i=0}a_j-\sum_{j\oplus i=1}a_j)(\sum_{k\oplus i=0}b_k-\sum_{k\oplus i=1}b_k)\\ &= (\sum_{j\oplus i=0}a_j)(\sum_{k\oplus i=0}b_k)-(\sum_{j\oplus i=0}a_j)(\sum_{k\oplus i=1}b_k)-(\sum_{j\oplus i=1}a_j)(\sum_{k\oplus i=0}b_k)+(\sum_{j\oplus i=1}a_j)(\sum_{k\oplus i=1}b_k)\\ &= (\sum_{(j\operatorname{xor} k)\oplus i=0}a_jb_k)-(\sum_{((j\operatorname{xor} k)\oplus i=1}a_jb_k)\\ &= (\sum_{(j\operatorname{xor} k)\oplus i=0}c_{j\operatorname{xor} k})-(\sum_{((j\operatorname{xor} k)\oplus i=1}c_{j\operatorname{xor} k})\\ &= fwt[c]_ i \end{aligned} \]

正变换:\(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}\)

代码上,所有的正逆变换都可以合并,注意取模,注意修改的顺序即可。

模板P4717

点击查看代码
#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;
}
posted @ 2022-05-03 17:32  cunzai_zsy0531  阅读(267)  评论(0编辑  收藏  举报