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

Page Views Count

集合幂级数#

定义#

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

定义从集合到数值的映射 f,则集合幂级数 f=S2UfSxSxS 只是占位符,用来表示 S 对应位置,并无实际意义。

运算#

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

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

h=fg=L2UfLxLR2UgRxR

对于某一项而言:

hS=L,R2U[LR=S]fLgR

于是整体:

h=S2UL,R2U[LR=S]aLbRxS

应用#

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

子集相关运算#

高维前(后)缀和#

f(S)=TSg(T)

f(S)=STg(T)

对于第一个式子,枚举当前位置 k,假设已知前 k1 位的和,即 f(S) 得到的贡献 g(T)T 在前 k1 位是 S 的子集,剩余位置与 S 相同,计算第 k 位实际上是这一位是 0 的答案贡献到是 1 的位置。

而第二个式子则是 10 的贡献。

//前缀
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)=TS(1)|S||T|f(T)

g(S)=ST(1)|T||S|f(T)

只需要在转移时增加一个系数 1,至于没有次数的原因,考虑 TS 的靠拢过程应当是增补 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#

或卷积(集合并卷积)#

考虑构造 f^S=TSfT,则 fS=TS(1)|S||T|f^T

h^S=TSL,R2U[LR=T]fLgR=L,R2U[LRS]fLgR=L,R2U[LS][RS]fLgR=L2U[LS]fLR2U[RS]gR=f^Sg^S

于是做法是先构造出 f^g^,乘出 h^,再逆变换回 h,这个逆变换称为 FMI。

与卷积(集合交卷积)#

考虑构造 f^S=STfT,则 fSST(1)|S||T|f^T

把或卷积稍微改变一下。

h^S=STL,R2U[LR=T]fLgR=L,R2U[SLR]fLgR=L,R2U[SL][SR]fLgR=L2U[SL]fLR2U[SR]gR=f^Sg^S

点击查看代码
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 是一个纯粹的线性求和,因此经过若干次运算后规模仍然是 2U

这使得我们在进行多次乘法时,可以全部 FMT 后求积再 FMI,而不必要像 FFT 一样每次都在系数与点值之间来回切换。

快速沃尔什变换 FWT#

异或卷积(集合对称差卷积)#

先证明一个定理:

[S=]=T2U(1)|ST|

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

这样要求:

hS=L,R2U[LR=S]fLgR=L,R2U[LRS=]fLgR=L,R2U12nT2U(1)|(LRS)T|fLgR=12nL,R2UT2U(1)|LT|(1)|RT|(1)|ST|fLgR=12nT2U(1)|ST|L2U(1)|LT|fLR2U(1)|RT|gR

仿照上面推导式子的方法,设 f^S=T2U(1)|ST|fT,代入得:

hS=12nT2U(1)|ST|f^Tg^T=12nT2U(1)|ST|h^T

也就顺理成章地得到了 FWT 以及 IFWT 的基本式子,而二者只差了一个系数。

接下来继续模仿上面卷积时的递推方法,假定已经处理了前 k1 位(不同于前面的子集与超集,这里前 k1 位表示只有前 k1 位有不同),pq 二者只在第 k 位不同,且 p+2k1=q,则二者经过某种运算一定可以计算出前 k 位的答案。

考虑对于 p 而言由 k1 扩展到 k,对交集大小这个次数没有任何贡献,因此直接求和即可;而对于 q 而言,当且仅当在第 k 位能交集出 1 时,才会产生负贡献,而负贡献就来自于前 k1 位的 q,写成表达式:

{fp=fp+fqfq=fpfq

更为形式化的说,枚举至 k 时,令 kS,则:

{fS(k)=fS(k1)+fS{k}(k1)fS(k)=fS(k1)fS{k}(k1)

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

子集卷积#

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

hS=L,R2U[LR=S][LR=]fLgR

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

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

hi+j,Si=0nj=0nL,R2U[LR=S]fi,Lgj,R

由于当且仅当第一维与第二维的 popcount 相等才能产生贡献,因此将初始值定在 popcount 处,其位置为 0

容易发现增加两个求和号以及第一维对 FMT 的推导过程没有影响,只是在计算乘积时要求贡献至 i+j 位置。

这样做 n+1 次 FMT 以及最后做 n+1 次 FMI 即可,复杂度 O(n22n)

点击查看代码
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;
}

参考资料#

作者:SoyTony

出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_Polynomial_2.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   SoyTony  阅读(181)  评论(5编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示