【学习笔记】多项式 2:集合幂级数
集合幂级数
定义
定义集合 \(U=\{1,2,\cdots,n\}\),\(2^U\) 表示 \(U\) 的所有子集构成的集合。
定义从集合到数值的映射 \(f\),则集合幂级数 \(f=\sum_{S\in 2^{U}} f_{S}x^S\),\(x^S\) 只是占位符,用来表示 \(S\) 对应位置,并无实际意义。
运算
加法运算同正常形式幂级数相同,对应位置加和即可。
乘法运算的系数仍然是对应相乘,而下标的运算则是按照一种关于集合的运算 \(\oplus\)。
对于某一项而言:
于是整体:
应用
多数情况下我们将状态压成二进制并卷积进行计算,因此应用较广的是以位运算作为 \(\oplus\),也称为 位运算卷积。
子集相关运算
高维前(后)缀和
对于第一个式子,枚举当前位置 \(k\),假设已知前 \(k-1\) 位的和,即 \(f(S)\) 得到的贡献 \(g(T)\) 中 \(T\) 在前 \(k-1\) 位是 \(S\) 的子集,剩余位置与 \(S\) 相同,计算第 \(k\) 位实际上是这一位是 \(0\) 的答案贡献到是 \(1\) 的位置。
而第二个式子则是 \(1\) 到 \(0\) 的贡献。
//前缀
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(i&(1<<k)) f[i]+=f[i^(1<<k)];
}
}
//后缀
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(!(i&(1<<k))) f[i]+=f[i^(1<<k)];
}
}
高维前(后)缀差分
子集和超集反演之后,得到:
只需要在转移时增加一个系数 \(-1\),至于没有次数的原因,考虑 \(T\) 到 \(S\) 的靠拢过程应当是增补 \(1\)(作为子集)或削去 \(1\)(作为超集),这样转移次数就是两个集合大小之差,每次转移乘 \(-1\),正符合上面式子的次数。
//前缀
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(i&(1<<k)) f[i]-=f[i^(1<<k)];
}
}
//后缀
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(!(i&(1<<k))) f[i]-=f[i^(1<<k)];
}
}
快速莫比乌斯变换 FMT
或卷积(集合并卷积)
考虑构造 \(\hat{f}_S=\sum_{T\subseteq S}f_T\),则 \(f_S=\sum_{T\subseteq S}(-1)^{|S|-|T|}\hat{f}_T\)。
于是做法是先构造出 \(\hat{f}\) 与 \(\hat{g}\),乘出 \(\hat{h}\),再逆变换回 \(h\),这个逆变换称为 FMI。
与卷积(集合交卷积)
考虑构造 \(\hat{f}_S=\sum_{S\subseteq T}f_T\),则 \(f_S\sum_{S\subseteq T}(-1)^{|S|-|T|}\hat{f}_T\)。
把或卷积稍微改变一下。
点击查看代码
int n;
inline void FMT_or(int *f,int c){
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(i&(1<<k)) f[i]=(f[i]+1ll*c*f[i^(1<<k)]%mod)%mod;
}
}
}
inline void FMT_and(int *f,int c){
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(!(i&(1<<k))) f[i]=(f[i]+1ll*c*f[i^(1<<k)]%mod)%mod;
}
}
}
int a[maxn],b[maxn];
int F[maxn],G[maxn],H[maxn];
int main(){
n=read();
for(int i=0;i<(1<<n);++i) a[i]=read();
for(int i=0;i<(1<<n);++i) b[i]=read();
for(int i=0;i<(1<<n);++i) F[i]=a[i],G[i]=b[i];
FMT_or(F,1),FMT_or(G,1);
for(int i=0;i<(1<<n);++i) H[i]=1ll*F[i]*G[i]%mod;
FMT_or(H,mod-1);
for(int i=0;i<(1<<n);++i) printf("%d ",H[i]);
printf("\n");
for(int i=0;i<(1<<n);++i) F[i]=a[i],G[i]=b[i];
FMT_and(F,1),FMT_and(G,1);
for(int i=0;i<(1<<n);++i) H[i]=1ll*F[i]*G[i]%mod;
FMT_and(H,mod-1);
for(int i=0;i<(1<<n);++i) printf("%d ",H[i]);
printf("\n");
return 0;
}
特殊性质
注意到与多项式乘法中的 FFT 不同的是,FMT 是一个纯粹的线性求和,因此经过若干次运算后规模仍然是 \(2^U\)。
这使得我们在进行多次乘法时,可以全部 FMT 后求积再 FMI,而不必要像 FFT 一样每次都在系数与点值之间来回切换。
快速沃尔什变换 FWT
异或卷积(集合对称差卷积)
先证明一个定理:
证明只需考虑 \(S\) 的某个元素 \(i\),除 \(i\) 外完全相同的两个 \(T\) 贡献和为 \(0\),空集除外。
这样要求:
仿照上面推导式子的方法,设 \(\hat{f}_S=\sum_{T\in 2^U}(-1)^{|S\cap T|} f_T\),代入得:
也就顺理成章地得到了 FWT 以及 IFWT 的基本式子,而二者只差了一个系数。
接下来继续模仿上面卷积时的递推方法,假定已经处理了前 \(k-1\) 位(不同于前面的子集与超集,这里前 \(k-1\) 位表示只有前 \(k-1\) 位有不同),\(p\) 与 \(q\) 二者只在第 \(k\) 位不同,且 \(p+2^{k-1}=q\),则二者经过某种运算一定可以计算出前 \(k\) 位的答案。
考虑对于 \(p\) 而言由 \(k-1\) 扩展到 \(k\),对交集大小这个次数没有任何贡献,因此直接求和即可;而对于 \(q\) 而言,当且仅当在第 \(k\) 位能交集出 \(1\) 时,才会产生负贡献,而负贡献就来自于前 \(k-1\) 位的 \(q\),写成表达式:
更为形式化的说,枚举至 \(k\) 时,令 \(k\notin S\),则:
也只有最后一项的两个集合会多出一个 \(-1\) 而取到负值。
点击查看代码
inline int q_pow(int A,int B,int P){
int res=1;
while(B){
if(B&1) res=1ll*res*A%P;
A=1ll*A*A%P;
B>>=1;
}
return res;
}
int n;
inline void FWT_xor(int *f,bool type){
for(int d=1;d<(1<<n);d<<=1){
for(int i=0;i<(1<<n);i+=d<<1){
for(int j=0;j<d;++j){
int x=f[i+j],y=f[i+d+j];
f[i+j]=(x+y)%mod,f[i+d+j]=(x-y+mod)%mod;
}
}
}
if(!type){
int inv=q_pow(1<<n,mod-2,mod);
for(int i=0;i<(1<<n);++i) f[i]=1ll*f[i]*inv%mod;
}
}
int F[maxn],G[maxn],H[maxn];
int main(){
n=read();
for(int i=0;i<(1<<n);++i) F[i]=read();
for(int i=0;i<(1<<n);++i) G[i]=read();
FWT_xor(F,1),FWT_xor(G,1);
for(int i=0;i<(1<<n);++i) H[i]=1ll*F[i]*G[i]%mod;
FWT_xor(H,0);
for(int i=0;i<(1<<n);++i) printf("%d ",H[i]);
printf("\n");
return 0;
}
子集卷积
在或卷积的基础上,增加了新的限制:
实际上就是将 \(S\) 划分成两部分乘积再求和。
一个充要条件是 \(|L|+|R|=|S|\),于是可以将无交修改为大小形如和卷积,即:
由于当且仅当第一维与第二维的 \(\mathrm{popcount}\) 相等才能产生贡献,因此将初始值定在 \(\mathrm{popcount}\) 处,其位置为 \(0\)。
容易发现增加两个求和号以及第一维对 FMT 的推导过程没有影响,只是在计算乘积时要求贡献至 \(i+j\) 位置。
这样做 \(n+1\) 次 FMT 以及最后做 \(n+1\) 次 FMI 即可,复杂度 \(O(n^22^n)\)。
点击查看代码
int n;
inline void FMT_or(int *f,int c){
for(int k=0;k<n;++k){
for(int i=0;i<(1<<n);++i){
if(i&(1<<k)) f[i]=(f[i]+1ll*c*f[i^(1<<k)]%mod)%mod;
}
}
}
#define lowbit(x) (x&-x)
int popcount[maxn];
int F[21][maxn],G[21][maxn],H[21][maxn];
int main(){
n=read();
for(int i=1;i<(1<<n);++i) popcount[i]=popcount[i^lowbit(i)]+1;
for(int i=0;i<(1<<n);++i) F[popcount[i]][i]=read();
for(int i=0;i<(1<<n);++i) G[popcount[i]][i]=read();
for(int i=0;i<=n;++i) FMT_or(F[i],1),FMT_or(G[i],1);
for(int i=0;i<=n;++i){
for(int j=0;i+j<=n;++j){
for(int k=0;k<(1<<n);++k){
H[i+j][k]=(H[i+j][k]+1ll*F[i][k]*G[j][k])%mod;
}
}
}
for(int i=0;i<=n;++i) FMT_or(H[i],mod-1);
for(int i=0;i<(1<<n);++i) printf("%d ",H[popcount[i]][i]);
printf("\n");
return 0;
}