从零开始的简单集合幂级数

集合幂级数 学习笔记

0 集合幂级数与高维前后缀和

定义集合幂级数为形如 \(\sum_{i\sube U} a_ix^i\) 的幂级数。即对于每一个子集,我们都有一个值 \(a_i\),就可以有一个形式幂级数。这样我们就能定义对于此类幂级数的各类卷积和各类复合。

\(a\) 的高维前缀和 \(A\)

\[A_i=\sum_{j\subseteq i} a_j \]

这个可以通过 \(O(2^nn)\) 的方法得到。我们也可以用同样的复杂度用 \(A\) 反求出 \(a\)

不止有前缀和,同样的,我们可以得到一个高维后缀和。对于 \(a\) 的高维后缀和 \(A\) 我们有

\[A_i=\sum_{i\sube j} a_j \]

1 或/与卷积与 FMT

这样求出集合幂级数的高维前缀和的变换为 FMT,逆变换则为 IFMT。

我们闲来无事,把两个前缀和乘起来

\[A_iB_i=\sum_{j\sube i}\sum_{k\sube i}a_jb_k=\sum_{j\cup k\sube i} a_jb_k=\sum_{x\sube i} \sum_{j\cup k=x} a_jb_k \]

若设后面那部分为 \(c_i\),我们就可以发现

\[A_iB_i=\sum_{x\sube i}c_i=C_i \]

这个 \(c_i\) 正是 \(a_i\)\(b_i\)或卷积

对于 \(a_i\)\(b_i\),其或卷积为

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

于是我们就有了 \(O(2^nn)\) 的求出 \(a_i\)\(b_i\) 的或卷积的办法。

同样的,我们也有与卷积。设 \(d_i\)\(a_i\)\(b_i\)与卷积,则

\[d_i=\sum_{j\cap k=i} a_jb_k \]

同或卷积相似,\(d_i\) 的后缀和是 \(a_i\)\(b_i\) 各自的后缀和的点积。

void fmt_or(int *a,int c) {
	rep(i,0,n-1) rep(j,0,s) if(j&(1<<i)) a[j]+=c*a[j^(1<<i)];
}
void conv_or(int *a,int *b,int *c) {
    fmt_or(a,1), fmt_or(b,1);
    rep(i,0,s) c[i]=a[i]*b[i];
    fmt_or(c,-1);
}
void fmt_and(int *a,int c) {
	rep(i,0,n-1) rep(j,0,s) if(j&(1<<i)) a[j^(1<<i)]+=c*a[j];
}
void conv_and(int *a,int *b,int *c) {
    fmt_and(a,1), fmt_and(b,1);
    rep(i,0,s) c[i]=a[i]*b[i];
    fmt_and(c,-1);
}

2 异或卷积与 FWT

异或卷积的定义和上面几乎相同,只不过把与/或换成了异或。

定义一个算子 \(\operatorname{FWT}(a)\),得到的结果也为集合幂级数

\[\operatorname{FWT}(a)_i=\sum_{j\sube U} (-1)^{\mid i \cap j \mid} a_j \]

\(c\)\(a\)\(b\) 异或卷积的结果,那么有 \(\operatorname{FWT}(c)_i=\operatorname{FWT}(a)_i\cdot \operatorname{FWT}(b)_i\)

\[\begin{aligned} \operatorname{FWT}(c)_i &= \sum_{j} (-1)^{|i\and j|}c_j\\ &=\sum_j (-1)^{|i\and j|} \sum_{k,l} [k\oplus l=j] a_kb_l\\ &=\sum_{k,l} (-1)^{|(k\oplus l)\and i|} a_kb_l\\ &=\sum_{k} (-1)^{|k\and i|}a_k \sum_{l} (-1)^{|l\and i|}b_l\\ &=\operatorname{FWT}(a)_i\cdot \operatorname{FWT}(b)_i \end{aligned} \]

现在我们希望能用较低的复杂度计算 \(\operatorname{FWT}(a)\)。考虑每一位给结果带来的更新。对于第 \(i\) 位和不包含第 \(i\) 位的集合 \(s\),我们发现有一个类似蝴蝶变换的操作:令 \(x=a_s, y=a_{s+2^i}\),则有新的 \(a_s=x+y\)\(a_{s+2^i}=x-y\)

于是我们只需要枚举这一位,然后枚举所有不包含 \(i\) 的集合,做一次如上变换即可。代码和 FFT 十分的相似(因为也可以用 FFT 的分治思想去理解这种变换过程)。

注意逆变换的过程相当于 \(a_s=\frac{x+y}{2}\)\(a_{s+2^i}=\frac{x-y}{2}\)

void fwt_xor(int *a,int c) {
	for(int i=1;i<=s;i<<=1) for(int j=0;j<=s;j+=(i<<1)) rep(k,j,j+i-1) {
		int x=a[k], y=a[k+i];
		a[k]=c*(x+y)%mod, a[k+i]=c*(x-y)%mod;
	}
    rep(i,0,s) a[i]=(a[i]%mod+mod)%mod;
}
void conv_xor(int *a,int *b,int *c) {
	fwt_xor(a,1), fwt_xor(b,1);
    rep(i,0,s) c[i]=a[i]*b[i]%mod;
    fwt_xor(c,(mod+1)/2);
}

3 子集卷积

有时候我们需要求类似以下的式子

\[c_i=\sum_{j\or k=i\\j\and k=\emptyset} a_{j}b_{k} \]

即将 \(i\) 恰好拆分成两个没有相同元素的子集 \(j,k\),然后做卷积。这就是子集卷积

考虑将所有子集按照集合元素个数进行分类。设 \(P_x\) 表示对于 \(a\),元素个数为 \(x\) 的所有集合的形式幂级数。

\[P_{x,i}= \begin{cases} a_i & |i|=x\\ 0 & |i|\neq x \end{cases} \]

也设 \(Q_x\) 为对于 \(b\) 的这样的元素个数为 \(x\) 的所有集合的形式幂级数,\(R_x\) 为对于 \(c\) 的这样的形式幂级数。

那么我们要做的就是对于所有 \(x,y\),把 \(P_x\)\(Q_y\) 的或卷积加到 \(R\) 上。

我们可以预处理出所有 \(P_x\)\(Q_x\)\(\operatorname{FMT}\),这样就能做到复杂度 \(O(2^nn^2)\)

LOJ 上模板题的代码:https://loj.ac/s/1373726


以下内容需要指数级生成函数的前置知识,不然会较难看懂。

4 集合幂级数 EXP

有时我们要处理类似以下的式子

\[c_i=\sum_{j_1\or j_2\or j_3 \or\dots\or j_k=i\\|j_1|+|j_2|+\dots+|j_k|=|i|} \prod_l a_{j_l} \]

即对集合幂级数做一个类似 exp 的操作。

我们考虑将所有子集按照其最高位分类,形成 \(n\) 组,而每一组中最多选择一个出来,这样就能转换成和普通子集卷积比较类似的问题。设 \(p_i\) 表示最高位为 \(i\) 的组。

我们动态维护集合幂级数 \(g\) 表示(从低位开始往高位合并)前 \(i-1\) 组合并出来的结果,并且现在需要合并上第 \(i\) 组。我们发现这个合并过程其实就是一个子集卷积,并且此时 \(g\)\(p_i\) 都只有 \(i\) 位,意味着单次合并复杂度是 \(O(2^ii^2)\) 的。

总复杂度为 \(O(\sum 2^ii^2)=O(2^nn^2)\)

和多项式 exp 一样,我们需要注意除掉 \(i!\)

5 多项式复合集合幂级数

EI 在 CF 上发的原 blog:https://codeforces.com/blog/entry/92183

image-20220207211433065

没关系,本来就学不会 binary search,学一学这东西也无妨。

是根据 xtq 翻讲的版本学的,和原版难免有偏差,但是 xtq 讲的真心非常好(。


需要计算 \(c=\sum_i f_ia^i\)

和 exp 类似,我们考虑按最高位分组。设 \(F_x\) 表示最高位为 \(x\) 的组的集合幂级数。

我们还是采取从低位到高位的合并方法。不过不同于上面的 exp,这里特殊的地方在于每个 \(f_i\)\(a\) 卷了几次的要求是不一样的。\(f_i\) 的贡献必须限定在卷了 \(i\) 次的东西上。

\(G_{i,j}\) 表示,目前考虑前 \(i\) 组(即 \(0\)\(i-1\) 位),并且还需要再卷 \(j\) 次的结果。初始值有 \(G_{0,j}=f_j\),答案为 \(G_{n,0}\)

转移比较简单,\(G_{i,j}\) 可以贡献给 \(G_{i+1,j}\)\(i\) 这一位不卷),\(G_{i+1,j-1}\)\(i\) 这位卷一次),具体而言有

\[\begin{aligned} G_{i+1,j} &\leftarrow G_{i,j}\\ G_{i+1,j-1} &\leftarrow G_{i,j} * F_{i} \end{aligned} \]

乍一看这复杂度很不对,但实际上我们梳理一下有用的 \(G_{i,j}\) 满足 \(j\le n-i\),所以实际上复杂度为 \(O(\sum (n-k)O(k^22^k))=O(n^22^n)\),因为 \(\sum k\times 2^{-k}\) 是收敛的。

有了这个神奇的工具,我们就能用 \(O(n^22^n)\) 算许多有趣的东西了。

LOJ154 集合划分计数

这个划分可以想到类似 EGF 的东西。

大小为 \(k\) 的划分相当于一个 \(F^k\)。所以我们实际上要求的是

\[[x^{S}]\sum_{i}[i\le k]\frac{F^i}{i!} \]

我们用 \([x^i]G=[i\le k]\frac{1}{i!}\) 去复合即可。

namespace SetP {
	void fmt(vi &a,int n,int c) { //FMT/IFMT
		int s=(1<<n)-1;
		for(int i=1;i<=s;i<<=1) jmp(j,0,s,i<<1) rep(k,j,j+i-1)
			a[k+i]=(1ll*a[k+i]+c*a[k]+mod)%mod;
	}
	vector<vi> trans(vi &a,int n) { //按popcount分组
		int s=(1<<n)-1;
		vector<vi>r; r.resize(n+1);
		rep(i,0,n) r[i].resize(s+1);
		rep(i,0,s) r[popc[i]][i]=a[i];
		return r;
	}
	vi itrans(vector<vi> &a,int n) { //分组重新变成集合幂级数
		int s=(1<<n)-1;
		vi r; r.resize(s+1);
		rep(i,0,s) r[i]=a[popc[i]][i];
		return r;
	}
	vector<vi> conv(vector<vi> &a,vector<vi>&b,int n) { //分组的卷积
		int s=(1<<n)-1;
		vector<vi>r; r.resize(n+1);
		rep(i,0,n) r[i].resize(s+1);
		rep(i,0,n) rep(j,0,n-i) rep(k,0,s)
			r[i+j][k]=(r[i+j][k]+1ll*a[i][k]*b[j][k])%mod;
		return r;
	}
	vi comp(vi &a,vi &b,int n) { //多项式a复合集合幂级数b
		int s=(1<<n)-1;
		vector<vi> lst(n+1),cur(n+1),tb(n+1);
		rep(i,0,n) lst[i].resize(s+1), cur[i].resize(s+1), tb[i].resize(s+1);
		rep(i,0,n) lst[i][0]=a[i];
		rep(i,0,n-1) {
			vi af; af.resize(s+1);
			int s=(1<<i)-1, t=(1<<i+1)-1;
			rep(j,0,s) af[(s+1)|j]=b[(s+1)|j]; //处理出F
			vector<vi> pf=trans(af,i+1);
			rep(j,0,i) fmt(pf[j],i+1,1);
			rep(j,1,n-i) {
				vector<vi> plst=trans(lst[j],i+1);

				rep(k,0,i+1) fmt(plst[k],i+1,1);
				vector<vi> cvr=conv(plst,pf,i+1);
				
				rep(k,0,i+1) fmt(cvr[k],i+1,-1);
				cur[j-1]=itrans(cvr,i+1); //第二种转移
				rep(k,0,t) cur[j-1][k]=(cur[j-1][k]+lst[j-1][k])%mod; //第一种转移
			}
			rep(j,0,n-i-1) rep(k,0,t) lst[j][k]=cur[j][k];
		}
		return cur[0];
	}
}

posted @ 2022-02-08 09:16  LarsWerner  阅读(266)  评论(0编辑  收藏  举报