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

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

我觉得以后我行内 \(\LaTeX\) 应该加个空格。

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

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

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

\[h_i=\sum_{i=j\oplus k}f_jg_k \]

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

  1. 或运算

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

\[FWT(f)_i=\sum_{j|i=i}f_j \]

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

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

\[FWT(f)=\text{merge}(FWT(f_0),FWT(f_0)+FWT(f_1)) \]

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

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

\[\begin{aligned} &FWT(f)_i\times FWT(g)_i\\ =&\left(\sum_{j|i=i}f_j\right)\left(\sum_{k|i=i}g_k\right)\\ =&\sum_{j|i=i,k|i=i}f_jg_k\\ =&\sum_{(j|k)|i=i}f_jg_k\\ =&FWT(h)_i \end{aligned} \]

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

\[IFWT(f)=\text{merge}(IFWT(f_0),IFWT(f_1)-IFWT(f_0)) \]

显然两个可以扔到一起。

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=\sum_{j\&i=i}f_j \]

然后定义 \(f_0,f_1\) 同上,那么 \(f_1\)的超集就是自己的超集, \(f_0\)的超集就是自己的加上 \(f_1\) 的,于是类似或,我们得到了:

\[FWT(f)=\text{merge}(FWT(f_0)+FWT(f_1),FWT(f_1)) \]

\[IFWT(f)=\text{merge}(IFWT(f_0)-IFWT(f_1),IFWT(f_1)) \]

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. 异或运算

我们构造运算 \(i\oplus j\)\(i\ \&\ j\)\(1\) 的数量的奇偶性,那么发现这个运算满足:

\[(i\oplus k)\ \text{xor}\ (j\oplus k)=(i\ \text{xor}\ j)\oplus k \]

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

  1. \(i\oplus k\)\(1\) , \(j\oplus k\)\(0\),则 \(i,k\)\(1\) , \(j\)\(0\) , \((i\ \text{xor}\ j)\oplus k\)\(1\)。反过来同理。
  2. \(i\oplus k\)\(1\) , \(j\oplus k\)\(1\),则 \(i,j,k\)\(1\) , \((i\ \text{xor}\ j)\oplus k\)\(0\)
  3. \(i\oplus k\)\(0\) , \(j\oplus k\)\(0\),则 \(i,j\)\(0\) , \(k\)\(0\)\(1\) , \((i\ \text{xor}\ j)\oplus k\)\(0\)

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

\[FWT(f)_i=\sum_{i\oplus j=0}f_j-\sum_{i\oplus j=1}f_j \]

那么乘起来有:

\[\begin{aligned} &FWT(f)_i\times FWT(g)_i\\ =&\left(\sum_{i\oplus j=0}f_j-\sum_{i\oplus j=1}f_j\right)\times \left(\sum_{i\oplus k=0}g_k-\sum_{i\oplus k=1}g_k\right)\\ =&\left(\sum_{i\oplus j=0}f_j\right)\left(\sum_{i\oplus k=0}g_k\right)+\left(\sum_{i\oplus j=1}f_j\right)\left(\sum_{i\oplus k=1}g_k\right)-\left(\sum_{i\oplus j=1}f_j\right)\left(\sum_{i\oplus k=0}g_k\right)-\left(\sum_{i\oplus j=0}f_j\right)\left(\sum_{i\oplus k=1}g_k\right)\\ =&\sum_{(j\ \text{xor}\ k)\oplus i=0}f_jg_k-\sum_{(j\ \text{xor}\ k)\oplus i=1}f_jg_k\\ =&FWT(h)_i \end{aligned} \]

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

\[FWT(f)=\text{merge}(FWT(f_0)+FWT(f_1),FWT(f_0)-FWT(f_1)) \]

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

\[IFWT(f)=\text{merge}(\frac {IFWT(f_0)+IFWT(f_1)}2,\frac {IFWT(f_0)-IFWT(f_1)}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(n\log^2n)\) 的复杂度内求出形如

\[h_i=\sum_{j|k=i,j\&k=0}f_jg_k \]

这样的卷积形式。

首先或的要求我们直接 \(\text{or}\) 卷积就行。然后是与为 \(0\) 。我们可以多增加一维,记录每个数的 \(1\) 的个数,因为 \(|i\cap j|=0\Leftrightarrow |i|+|j|=|i\cup j|\) 。这样,我们把每个序列拆开,对每个位数做 \(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 @ 2022-09-03 19:53  gtm1514  阅读(303)  评论(0编辑  收藏  举报