集合幂级数 学习笔记
0 集合幂级数与高维前后缀和
定义集合幂级数为形如 \(\sum_{i\sube U} a_ix^i\) 的幂级数。即对于每一个子集,我们都有一个值 \(a_i\),就可以有一个形式幂级数。这样我们就能定义对于此类幂级数的各类卷积和各类复合。
则 \(a\) 的高维前缀和 \(A\) 是
这个可以通过 \(O(2^nn)\) 的方法得到。我们也可以用同样的复杂度用 \(A\) 反求出 \(a\)。
不止有前缀和,同样的,我们可以得到一个高维后缀和。对于 \(a\) 的高维后缀和 \(A\) 我们有
1 或/与卷积与 FMT
这样求出集合幂级数的高维前缀和的变换为 FMT,逆变换则为 IFMT。
我们闲来无事,把两个前缀和乘起来
若设后面那部分为 \(c_i\),我们就可以发现
这个 \(c_i\) 正是 \(a_i\) 和 \(b_i\) 的或卷积。
对于 \(a_i\) 和 \(b_i\),其或卷积为
于是我们就有了 \(O(2^nn)\) 的求出 \(a_i\) 和 \(b_i\) 的或卷积的办法。
同样的,我们也有与卷积。设 \(d_i\) 为 \(a_i\) 和 \(b_i\) 的与卷积,则
同或卷积相似,\(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)\),得到的结果也为集合幂级数
设 \(c\) 为 \(a\) 和 \(b\) 异或卷积的结果,那么有 \(\operatorname{FWT}(c)_i=\operatorname{FWT}(a)_i\cdot \operatorname{FWT}(b)_i\)。
现在我们希望能用较低的复杂度计算 \(\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 子集卷积
有时候我们需要求类似以下的式子
即将 \(i\) 恰好拆分成两个没有相同元素的子集 \(j,k\),然后做卷积。这就是子集卷积。
考虑将所有子集按照集合元素个数进行分类。设 \(P_x\) 表示对于 \(a\),元素个数为 \(x\) 的所有集合的形式幂级数。
也设 \(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
有时我们要处理类似以下的式子
即对集合幂级数做一个类似 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
没关系,本来就学不会 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\) 这位卷一次),具体而言有
乍一看这复杂度很不对,但实际上我们梳理一下有用的 \(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^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];
}
}