二进制卷积(FWT,子集卷积)

来解决一下二进制卷积相关。以下均假设序列长度为 2 的次幂。

我觉得以后我行内 LATEX 应该加个空格。

快速沃尔什变换(Fast Walsh Transform,FWT)

这个是拿来在 O(nlogn) 时间复杂度内处理位运算(与、或、异或)卷积的。

重申一遍卷积:两个序列 f,g 的卷积是

hi=i=jkfjgk

当中间的二元运算是三种位运算中的一种的时候,就变成了位运算卷积。我们一个一个讨论。

  1. 或运算

我们定义对于序列 f 的变换 FWT(f) 的第 i 项为

FWT(f)i=j|i=ifj

也就是我们要求以 i 的所有子集为下标的元素和。

考虑类似FFT的分治做法,我们设 f0 为所有二进制位开头为 0 的数(就是前一半), f1 为二进制位开头为 1 的数(后一半),那么前一半的子集就是它自己的子集,而后一半的子集除了后一半自己的,还有前一半对应位置的(因为它自己的最高位是 1 ,对应的最高位是 0 的仍然是它的子集)。所以我们有:

FWT(f)=merge(FWT(f0),FWT(f0)+FWT(f1))

其中 merge 表示把前后两个序列拼起来,加法表示对应位相加(就是最高位带 1 的项加上最高位不带 1 的项)。显然这个可以像FFT一样迭代。

然后就可以普通地把每一项对应相乘了。略证为什么直接乘是对的:

FWT(f)i×FWT(g)i=(j|i=ifj)(k|i=igk)=j|i=i,k|i=ifjgk=(j|k)|i=ifjgk=FWT(h)i

然后关于逆变换,我们知道 f0 的子集是它自己的, f1 的子集是它自己的减去 f0 的,那么我们类似上面的式子,有:

IFWT(f)=merge(IFWT(f0),IFWT(f1)IFWT(f0))

显然两个可以扔到一起。

void getor(int a[],int n,int tp){
    for(int mid=1;mid<n;mid<<=1){
        for(int i=0;i<n;i+=(mid<<1)){
            for(int j=0;j<mid;j++){
                a[i+j+mid]=(a[i+j+mid]+1ll*a[i+j]*tp)%mod;
            }
        }
    }
}
  1. 与运算

类似或运算,我们定义序列 f 的变换 FWT(f) 的第 i 项为

FWT(f)i=j&i=ifj

然后定义 f0,f1 同上,那么 f1的超集就是自己的超集, f0的超集就是自己的加上 f1 的,于是类似或,我们得到了:

FWT(f)=merge(FWT(f0)+FWT(f1),FWT(f1))

IFWT(f)=merge(IFWT(f0)IFWT(f1),IFWT(f1))

void getand(int a[],int n,int tp){
    for(int mid=1;mid<n;mid<<=1){
        for(int i=0;i<n;i+=(mid<<1)){
            for(int j=0;j<mid;j++){
                a[i+j]=(a[i+j]+1ll*a[i+j+mid]*tp)%mod;
            }
        }
    }
}
  1. 异或运算

我们构造运算 iji & j1 的数量的奇偶性,那么发现这个运算满足:

(ik) xor (jk)=(i xor j)k

证明考虑大力分讨即可。(我不想码字)
关于二进制第p位,有如下三种情况:

  1. ik1 , jk0,则 i,k1 , j0 , (i xor j)k1。反过来同理。
  2. ik1 , jk1,则 i,j,k1 , (i xor j)k0
  3. ik0 , jk0,则 i,j0 , k01 , (i xor j)k0

综上得证。那么定义序列 f 的变换 FWT(f) 的第 i 项为

FWT(f)i=ij=0fjij=1fj

那么乘起来有:

FWT(f)i×FWT(g)i=(ij=0fjij=1fj)×(ik=0gkik=1gk)=(ij=0fj)(ik=0gk)+(ij=1fj)(ik=1gk)(ij=1fj)(ik=0gk)(ij=0fj)(ik=1gk)=(j xor k)i=0fjgk(j xor k)i=1fjgk=FWT(h)i

然后考虑如何运算。仍然考虑分治的思想,如上定义 f0,f1 。对于 f0 ,最高位满足 0 & 0=0,0 & 1=0 ,所以前一半可以直接加上后一半的对应位置的值。而后一半有所不同,有1 & 0=0,1 & 1=1,所以自己的贡献是负的。即:

FWT(f)=merge(FWT(f0)+FWT(f1),FWT(f0)FWT(f1))

逆变换同理,解方程即可。

IFWT(f)=merge(IFWT(f0)+IFWT(f1)2,IFWT(f0)IFWT(f1)2)

void getxor(int a[],int n,int tp){
    for(int mid=1;mid<n;mid<<=1){
        for(int i=0;i<n;i+=(mid<<1)){
            for(int j=0;j<mid;j++){
                int x=a[i+j],y=a[i+j+mid];
                a[i+j]=1ll*(x+y)*tp%mod;
                a[i+j+mid]=1ll*(x-y+mod)*tp%mod;
            }
        }
    }
}

那么我们就有了板子的全部代码。

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
const int mod=998244353,inv2=(mod+1)>>1;
int a[1<<17],b[1<<17],n,p[1<<17],q[1<<17];
void getor(int a[],int n,int tp){
    for(int mid=1;mid<n;mid<<=1){
        for(int i=0;i<n;i+=(mid<<1)){
            for(int j=0;j<mid;j++){
                a[i+j+mid]=(a[i+j+mid]+1ll*a[i+j]*tp)%mod;
            }
        }
    }
}
void getand(int a[],int n,int tp){
    for(int mid=1;mid<n;mid<<=1){
        for(int i=0;i<n;i+=(mid<<1)){
            for(int j=0;j<mid;j++){
                a[i+j]=(a[i+j]+1ll*a[i+j+mid]*tp)%mod;
            }
        }
    }
}
void getxor(int a[],int n,int tp){
    for(int mid=1;mid<n;mid<<=1){
        for(int i=0;i<n;i+=(mid<<1)){
            for(int j=0;j<mid;j++){
                int x=a[i+j],y=a[i+j+mid];
                a[i+j]=1ll*(x+y)*tp%mod;
                a[i+j+mid]=1ll*(x-y+mod)*tp%mod;
            }
        }
    }
}
void get(){for(int i=0;i<(1<<n);i++)p[i]=a[i],q[i]=b[i];}
void calc(){for(int i=0;i<(1<<n);i++)p[i]=1ll*p[i]*q[i]%mod;}
void print(){for(int i=0;i<(1<<n);i++)printf("%d ",p[i]);printf("\n");}
int main(){
    scanf("%d",&n);
    for(int i=0;i<(1<<n);i++)scanf("%d",&a[i]);
    for(int i=0;i<(1<<n);i++)scanf("%d",&b[i]);
    get();getor(p,1<<n,1);getor(q,1<<n,1);calc();getor(p,1<<n,mod-1);print();
    get();getand(p,1<<n,1);getand(q,1<<n,1);calc();getand(p,1<<n,mod-1);print();
    get();getxor(p,1<<n,1);getxor(q,1<<n,1);calc();getxor(p,1<<n,inv2);print();
}

子集卷积

子集卷积可以在 O(nlog2n) 的复杂度内求出形如

hi=j|k=i,j&k=0fjgk

这样的卷积形式。

首先或的要求我们直接 or 卷积就行。然后是与为 0 。我们可以多增加一维,记录每个数的 1 的个数,因为 |ij|=0|i|+|j|=|ij| 。这样,我们把每个序列拆开,对每个位数做 FWT ,最后卷积起来再 FWT 回去就行了。具体的看代码。

int main(){
    scanf("%d",&n);
    for(int i=0;i<(1<<n);i++)scanf("%d",&a[__builtin_popcount(i)][i]);
    for(int i=0;i<(1<<n);i++)scanf("%d",&b[__builtin_popcount(i)][i]);
    //__builtin_popcount()是内置的统计数字二进制位中1的个数的函数 应该是O(1)的
    for(int i=0;i<=n;i++){
        getor(a[i],1<<n,1),getor(b[i],1<<n,1);
    }
    for(int i=0;i<=n;i++){
        for(int j=0;j<=i;j++){
            for(int k=0;k<(1<<n);k++){
                p[i][k]=(p[i][k]+1ll*a[j][k]*b[i-j][k]%mod)%mod;//对每一位变换之后按照定义乘
            }
        }
    }
    for(int i=0;i<=n;i++)getor(p[i],1<<n,mod-1);//逆变换回来
    for(int i=0;i<(1<<n);i++)printf("%d ",p[__builtin_popcount(i)][i]);
}
posted @   gtm1514  阅读(403)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示