Loading

【学习笔记】多项式 2:集合幂级数

Page Views Count

集合幂级数

定义

定义集合 \(U=\{1,2,\cdots,n\}\)\(2^U\) 表示 \(U\) 的所有子集构成的集合。

定义从集合到数值的映射 \(f\),则集合幂级数 \(f=\sum_{S\in 2^{U}} f_{S}x^S\)\(x^S\) 只是占位符,用来表示 \(S\) 对应位置,并无实际意义。

运算

加法运算同正常形式幂级数相同,对应位置加和即可。

乘法运算的系数仍然是对应相乘,而下标的运算则是按照一种关于集合的运算 \(\oplus\)

\[h=f\cdot g=\sum_{L\in 2^U}f_Lx^L\cdot\sum_{R\in 2^U}g_Rx^R \]

对于某一项而言:

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

于是整体:

\[h=\sum_{S\in 2^U}\sum_{L,R\in 2^U}[L\oplus R=S]a_Lb_R x^S \]

应用

多数情况下我们将状态压成二进制并卷积进行计算,因此应用较广的是以位运算作为 \(\oplus\),也称为 位运算卷积

子集相关运算

高维前(后)缀和

\[f(S)=\sum_{T\subseteq S} g(T) \]

\[f(S)=\sum_{S\subseteq T}g(T) \]

对于第一个式子,枚举当前位置 \(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)];
    }
}

高维前(后)缀差分

子集和超集反演之后,得到:

\[g(S)=\sum_{T\subseteq S}(-1)^{|S|-|T|} f(T) \]

\[g(S)=\sum_{S\subseteq T}(-1)^{|T|-|S|} f(T) \]

只需要在转移时增加一个系数 \(-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\)

\[\begin{aligned} \hat{h}_S&=\sum_{T\subseteq S}\sum_{L,R\in 2^U}[L\cup R=T] f_Lg_R\\ &=\sum_{L,R\in 2^U}[L\cup R\subseteq S] f_Lg_R\\ &=\sum_{L,R\in 2^U}[L\subseteq S][R\subseteq S] f_Lg_R\\ &=\sum_{L\in 2^U} [L\subseteq S] f_L\sum_{R\in 2^U} [R\subseteq S] g_R\\ &=\hat{f}_S\hat{g}_S \end{aligned}\]

于是做法是先构造出 \(\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\)

把或卷积稍微改变一下。

\[\begin{aligned} \hat{h}_S&=\sum_{S\subseteq T}\sum_{L,R\in 2^U}[L\cap R=T] f_Lg_R\\ &=\sum_{L,R\in 2^U}[S\subseteq L\cap R] f_Lg_R\\ &=\sum_{L,R\in 2^U}[S\subseteq L][S\subseteq R] f_Lg_R\\ &=\sum_{L\in 2^U} [S\subseteq L] f_L\sum_{R\in 2^U} [S\subseteq R] g_R\\ &=\hat{f}_S\hat{g}_S \end{aligned}\]

点击查看代码
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=\varnothing]=\sum_{T\in 2^U}(-1)^{|S\cap T|} \]

证明只需考虑 \(S\) 的某个元素 \(i\),除 \(i\) 外完全相同的两个 \(T\) 贡献和为 \(0\),空集除外。

这样要求:

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

仿照上面推导式子的方法,设 \(\hat{f}_S=\sum_{T\in 2^U}(-1)^{|S\cap T|} f_T\),代入得:

\[\begin{aligned} h_S&=\dfrac{1}{2^n}\sum_{T\in 2^U}(-1)^{|S\cap T|}\hat{f}_T\hat{g}_T\\ &=\dfrac{1}{2^n}\sum_{T\in 2^U}(-1)^{|S\cap T|} \hat{h}_T \end{aligned}\]

也就顺理成章地得到了 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\),写成表达式:

\[\begin{cases} f_p=f_p+f_q\\ f_q=f_p-f_q \end{cases}\]

更为形式化的说,枚举至 \(k\) 时,令 \(k\notin S\),则:

\[\begin{cases} f_S^{(k)}=f_S^{(k-1)}+f_{S\cup\{k\}}^{(k-1)}\\ f_S^{(k)}=f_S^{(k-1)}-f_{S\cup\{k\}}^{(k-1)}\\ \end{cases}\]

也只有最后一项的两个集合会多出一个 \(-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;
}

子集卷积

在或卷积的基础上,增加了新的限制:

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

实际上就是将 \(S\) 划分成两部分乘积再求和。

一个充要条件是 \(|L|+|R|=|S|\),于是可以将无交修改为大小形如和卷积,即:

\[h_{i+j,S}\sum_{i=0}^n\sum_{j=0}^n\sum_{L,R\in 2^U}[L\cup R=S]f_{i,L}g_{j,R} \]

由于当且仅当第一维与第二维的 \(\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;
}

参考资料

posted @ 2023-02-03 11:53  SoyTony  阅读(140)  评论(5编辑  收藏  举报