[学习笔记] 子集卷积相关

UltiMadow

2021-08-01 20:43:02

Algo. & Theory

对于二维前缀和,我们一般有两种计算方法

第一种可以使用容斥,s_{i,j}=s_{i-1,j}+s_{i,j-1}-s_{i-1,j-1}+a_{i,j}

第二种,我们可以先选一个维度进行前缀和:s_{i,j}=s_{i,j-1}+a_{i,j},接下来,再对第二个维度进行前缀和:s_{i,j}\leftarrow s_{i-1,j}+s_{i,j}

但对于更高维的情况,容斥就不适用了,但是可以用第二种方法,对每个维度依次做前缀和

用高维前缀和的思想,我们可以解决子集 dp 的问题

f_{s}=\sum\limits_{t\subseteq s}a_{t},则我们可以看做对 s 做了 n 维前缀和,便可以在 \mathcal O(n2^n) 的时间内解决这个问题

code:

for(int i=1;i<=n;i++)
    for(int s=0;s<(1<<n);s++)
        if(s&(1<<(i-1)))
            f[s]=(f[s]+f[s^(1<<(i-1))])%p;

FMT 可以解决 \mathrm{or} 卷积和 \mathrm{and} 卷积

\mathrm{or} 卷积为例,设 c_i=\sum\limits_{j\mathrm{or}k=i}a_jb_k

我们对 a_i 做一遍 n 维前缀和,f_i=\sum\limits_{j\subseteq i}a_j,得到 a 的 FMT,同样对 b_i 做一遍 n 维前缀和,得到 b 的 FMT g_i

接下来把 fg 点乘起来,h_i=f_ig_i

考虑 hc 的关系:h_i=f_ig_i=(\sum\limits_{j\subseteq i}a_j)(\sum\limits_{k\subseteq i}b_k)=\sum\limits_{j,k\subseteq i}a_jb_k=\sum\limits_{j\mathrm{or}k\subseteq i}a_jb_k,这便是 c_in 维前缀和

最后把 h 做一遍 n 维差分就可以得到 c

时间复杂度 \mathcal O(n2^n)

对于 \mathrm{and} 卷积,我们只要把 n 维前缀和换成 n 维后缀和即可

FWT 除了可以解决 \mathrm{or} 卷积和 \mathrm{and} 卷积之外,还可以解决 \mathrm{xor} 卷积

考虑是否存在一种类似 FFT 的变换,能把卷积变成点积

对于 \mathrm{or},考虑一种变换:

\mathrm{FWT}(A)=\begin{cases} A&(n=0)\\ \mathrm{FWT}(A_0),\mathrm{FWT}(A_1+A_0)&(n>0) \end{cases}

其中,A_0 表示第 n 位为 0 的序列,A_1 表示第 n 位为 1 的序列,+ 为两个序列按顺序分别相加

这样,我们就得到了 a 的 FWT f,考虑 fa 的关系

发现序列任何一位如果是 1 都加上了那一位是 0 的值,也就是我们得到了 f_i=\sum\limits_{j\subseteq i}a_j,和 FMT 一样

于是我们把 a 的 FWT fb 的 FWT g 点乘起来,得到的就是 c 的 FWT h

使用其逆变换:

\mathrm{IFWT}(A)=\begin{cases} A&(n=0)\\ \mathrm{IFWT}(A_0),\mathrm{IFWT}(A_1-A_0)&(n>0) \end{cases}

就可以得到 c

时间复杂度 \mathcal O(n2^n)

code:

void FWT(int *A,int sgn){
    for(int mid=1;mid<(1<<n);mid<<=1)
        for(int j=0,R=(mid<<1);j<(1<<n);j+=R)
            for(int k=0;k<mid;k++){
                if(sgn==1)A[j+k+mid]=A[j+k+mid]+A[j+k];
                else A[j+k+mid]=A[j+k+mid]-A[j+k];
            }
}

同样的,对于 \mathrm{and} 卷积,也有 FWT:

\mathrm{FWT}(A)=\begin{cases} A&(n=0)\\ \mathrm{FWT}(A_0+A_1),\mathrm{FWT}(A_0)&(n>0) \end{cases}

逆变换:

\mathrm{IFWT}(A)=\begin{cases} A&(n=0)\\ \mathrm{IFWT}(A_0-A_1),\mathrm{IFWT}(A_1)&(n>0) \end{cases}

code:

void FWT(int *A,int sgn){
    for(int mid=1;mid<(1<<n);mid<<=1)
        for(int j=0,R=(mid<<1);j<(1<<n);j+=R)
            for(int k=0;k<mid;k++){
                if(sgn==1)A[j+k]=A[j+k]+A[j+k+mid];
                else A[j+k]=A[j+k]-A[j+k+mid];
            }
}

此外,FWT 还可以做 \mathrm{xor} 卷积

在 xor-FWT 之前,先有一个结论

a\circ ba\ \mathrm{and}\ b 的二进制表示中 1 的位数(对 2 取模)

有性质 (a\circ x)\ \mathrm{xor}\ (a\circ y)=a\circ(x\ \mathrm{xor}\ y)

证明如下:

f(x) 表示 x 的二进制表示中 1 的位数

原式等价于 f(a\ \mathrm{and}\ x)+f(a\ \mathrm{and}\ y)\equiv f(a\ \mathrm{and}\ (x\ \mathrm{xor}\ y))\pmod 2

接下来,对 axy 的每一位大力分类讨论即可得证

构造一个变换:f_i=\sum\limits_{i\circ j=0}a_{j}-\sum\limits_{i\circ j=1}a_j

a 的变换为 fb 的变换为 gc 的变换为 h

\begin{aligned} f_ig_i&=(\sum\limits_{i\circ j=0}a_{j}-\sum\limits_{i\circ j=1}a_j)(\sum\limits_{i\circ k=0}b_{k}-\sum\limits_{i\circ k=1}b_k)\\ &=(\sum\limits_{i\circ j=0}a_{j}\sum\limits_{i\circ k=0}b_{k}+\sum\limits_{i\circ j=1}a_j\sum\limits_{i\circ k=1}b_k)-(\sum\limits_{i\circ j=0}a_{j}\sum\limits_{i\circ k=1}b_k+\sum\limits_{i\circ j=1}a_j\sum\limits_{i\circ k=0}b_{k})\\ &=\sum\limits_{i\circ (j\mathrm{xor}k)=0}a_{j}b_{k}-\sum\limits_{i\circ (j\mathrm{xor}k)=1}a_{j}b_{k}\\ &=h_i \end{aligned}

(第 3 行用到了上面那个性质)

考虑变换:

\mathrm{FWT}(A)=\begin{cases} A&(n=0)\\ \mathrm{FWT}(A_0+A_1),\mathrm{FWT}(A_0-A_1)&(n>0) \end{cases}

考虑最开始说的 a 的变换 f

f_xx 的第 i 位为 0,则无论 a_yy 的第 i 位为 0 还是 1,x\circ y 都不会改变,所以前面一部分的 a 的 FWT 和 f 是一致的

f_xx 的第 i 位为 1,则 a_yy 的第 i 位为 1 时 x\circ y 不会改变,而为 0 时会改变,于是需要把为 0 的部分减掉,而上面式子中 x\circ y 为 1 的系数为 -1,所以后面一部分 a 的 FWT 和 f 是一致的

于是得到 ab 的 FWT fg 之后就可以直接点乘得到 h

逆变换:

\mathrm{IFWT}(A)=\begin{cases} A&(n=0)\\ \mathrm{IFWT}(\frac{A_0+A_1}2),\mathrm{IFWT}(\frac{A_0-A_1}2)&(n>0) \end{cases}

code:

void FWT(int *A,int sgn){
    for(int mid=1;mid<(1<<n);mid<<=1)
        for(int j=0,R=(mid<<1);j<(1<<n);j+=R)
            for(int k=0;k<mid;k++){
                int x=A[j+k],y=A[j+k+mid];
                if(sgn==1)A[j+k]=x+y,A[j+k+mid]=x-y;
                else A[j+k]=(x+y)/2,A[j+k+mid]=(x-y)/2;
            }
}

考虑这个式子:c_i=\sum\limits_{j\mathrm{or}k=i,j\mathrm{and}k=\emptyset}a_jb_k

直接枚举子集可以做到 \mathcal O(3^n),不够优秀

如果没有 j\ \mathrm{and}\ k=\emptyset 的限制,则直接 \mathrm{or} 卷积即可完成

考虑转换一下限制

f(x)x 的二进制表示中 1 的位数

则原限制等价于 j\ \mathrm{or}\ k=if(j)+f(k)=f(j\ \mathrm{or}\ k)

于是将 a 数组根据 f(i) 拆开,a'_{i,s} 仅当 f(s)=i 时等于 a_s

同样将 b 数组拆开成 b'c 数组拆开成 c'

则考虑进行如下卷积:c'_{i,s}=\sum\limits_{j+k=i,p\mathrm{or}q=s} a'_{j,p}b'_{k,q},得到的 c'f(s)=i 的即为答案

于是对于所有的 a'b' 都做一遍 or-FWT,之后做卷积得到 c' 的 FWT,再都做一遍 or-IFWT,得到 c',就可以将答案提取出来了

时间复杂度 \mathcal O(n^22^n)

code:

for(int i=0;i<(1<<n);i++)scanf("%d",&a[pcnt[i]][i]);
for(int i=0;i<(1<<n);i++)scanf("%d",&b[pcnt[i]][i]);
for(int i=0;i<=n;i++)FWT(a[i],1),FWT(b[i],1);
for(int i=0;i<=n;i++)
    for(int j=0;j<(1<<n);j++)
        for(int k=0;k<=i;k++)
            c[i][j]=(c[i][j]+1ll*a[k][j]*b[i-k][j]%p)%p;
for(int i=0;i<=n;i++)FWT(c[i],-1);
for(int i=0;i<(1<<n);i++)printf("%d ",c[pcnt[i]][i]);
//pcnt[i]为f(i)

P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)

P6097 【模板】子集卷积

↑ 板子题 ↑

CF1034E Little C Loves 3 III

题意:在模 4 意义下求子集卷积

n\le 21

直接子集卷积是 \mathcal O(n^22^n) 的,不能通过此题,所以需要进行一些转化

f(x)x 的二进制表示中 1 的位数

考虑设 f_i=a_i\times 4^{f(i)},g_i=b_i\times 4^{f(i)}

接下来直接对 fg\mathrm{or} 卷积,得到 h_i=\sum\limits_{j\mathrm{or}k}a_jb_k\times 4^{f(j)+f(k)}

接下来把 h_i 除以 4^{f(i)} 并对 4 取模,就把 f(j)+f(k)\not =f(i) 的不合法情况去掉了(除以 4^{f(i)} 去掉了 f(j)+f(k)<f(i) 的情况,对 4 取模去掉了 f(j)+f(k)>f(i) 的情况,而 f(j)+f(k)=f(i) 的情况正好留下来了并且对 4 取了模)

那么时间复杂度 \mathcal O(n2^n)

CF1326F2 Wise Men (Hard Version)

题意:给定一个 n 个点的无向图,一个 n 个点的排列 p 能生成的 01 串的第 i 位为 p_ip_{i+1} 的连边情况,有边为 0,无边为 1
对于每个 01 串,求有多少种排列可以生成它

2\le n\le 18

考虑对于每个 01 串对排列的限制:第 i 位为 0 要求必须无边,为 1 要求必须有边

显然有边的限制比无边的要好处理一点,于是可以考虑去掉无边的限制

去掉必须无边的限制之后,我们发现答案其实求解的是一个集合 s 的高维后缀和,于是我们得到去掉限制的答案之后直接 and-IFWT/and-IFMT 就可以得到原来的答案了

如果只考虑必须有边,原题就被转化成了原图拆成若干条点不相交的链,对每个 01 串的贡献

发现这就是一个整数拆分问题,可以暴力 dfs 求解

f(x)x 的二进制表示中 1 的个数

f_{s}s 集合中的点组成一条链的方案数,可以用 dp 在 \mathcal O(n^22^n) 的时间内预处理出来

在整数拆分时,我们维护 g_ss 集合中的点组成若干条链,链的长度为当前 dfs 的整数拆分的方案数

我们发现 g 在每次转移的时候要和 f 做一次子集卷积,很慢

我们考虑在预处理时把 f_s 拆成 f_{f(s),s},然后预先对 f_{i} 跑一遍 or-FWT,这样每次转移 g 的时候只需要把 f_i 点乘进去就行了(这里 i 表示当前 dfs 的整数拆分)

这样计算出的 g 其实是 g 的 or-FWT,所以需要进行一次 or-IFWT,但是这样还是太慢了

发现计算时只需要全集的答案 g_U,而 or-FWT 其实是做了高维前缀和,所以对 g_U 做一次高维差分即可

这里算出的 g 是对于每个整数拆分的答案,我们可以把它用 hashmap 建立一个整数拆分到对应答案的映射

最后对于每个 01 串,都有一个对应的整数拆分序列,于是直接在 hashmap 里查询出答案即可

最后别忘了做一遍 and-IFWT

时间复杂度 \mathcal O(\sum\limits_{i\in p(n)}g(i)\times 2^n)

其中 p(n)n 的整数拆分集合,g(i) 为当前整数拆分方案拆出来数的个数