位运算卷积与集合幂级数重置版
又学了一下这东西,发现以前好多理解的不够深刻,于是决定补一篇博客。大概不算入门教程。
本文 \(\operatorname{or,and,xor}\) 表示位运算。
FWT 相关
FWT 类似 FFT,是对序列进行线性变换后,将卷积的计算简化为点积,从而极大减少计算的复杂度。
即假如我们通过某种方式得到了这个线性变换,则应该有:
其中 \(\oplus\) 是某种位运算。
因为是线性变换,所以要对应一个矩阵,不妨设 \(\mathbf{F}_{i,j}\) 表示 \(A_j\) 对 \(\operatorname{FWT}(A)_i\) 的贡献系数,即:
我们用这个带回刚刚的等式:
这样我们就能得到关于 \(\bf F\) 矩阵的一个等式:
注意到我们操作的是位运算,而位运算下,每一位都是独立的,所以在 \(k\) 进制下,我们只需要求出一个 \(k\times k\) 的 \(\bf F\) 即可。整个的 \(\bf F\) 可以通过相乘得到,比如对与 \(k\) 进制数 \(i,j\) 有:
其中 \(i_p\) 表示 \(i\) 在 \(k\) 进制下的第 \(p\) 位。
现在,假设我们通过某种奥妙重重的方法求出了需要的 \(\bf F\),然后就该求这个变换的式子了:
直接求是 \(\mathcal{O}(n^2)\) 的,感觉不如直接卷积,效率。因为每位是独立的,这启发我们拆开一位考虑,从而缩小问题的规模:
其中位运算在 \(k\) 进制意义下进行,\(j_0\) 表示最高位,\(j\backslash j_0\) 表示去掉最高位剩下的数。又发现后面的一坨和 \(\operatorname{FWT}\) 的形式很像,代入有:
其中 \(A_p\) 表示最高位为 \(p\) 的 \(A_j\) 组成的序列。这样一来,我们把原来的问题分成了 \(k\) 份子问题,可以在 \(\mathcal{O}(nk)\) 的复杂度内合并,从而得到一个 \(\mathcal{O}(nk\log n)\) 的算法。
\(\operatorname{IFWT}\) 也比较简单,求矩阵的逆就好了。不过这要求我们构造出的矩阵必须有逆。
当然,oi 里一般见不到 \(k\) 进制,大部分用的还是二进制下的 \(\operatorname{or,and,xor}\),所以研究一下这些位运算下 \(\bf F\) 的取值是很有必要的。当然,我们只需要知道 \(\mathbf{F}_{0,0},\mathbf{F}_{0,1},\mathbf{F}_{1,0},\mathbf{F}_{1,1}\) 就够了。
\(\operatorname{or}\)。有:
简单分讨一下:
就这么多限制了。加上有逆的限制还是能构造出来挺多矩阵的,不过常用的长这样:
它的逆矩阵是:
如果你比较熟悉位运算的相关内容,会发现这个矩阵表示的高位为 \(1\) 的只向自己做贡献,为 \(0\) 的向 \(0,1\) 都做贡献,是高维前缀和的形式。而下面的矩阵是高维差分。这大概也是这个矩阵常用的原因。代码最后三个一起放。
\(\operatorname{and}\)。还是简单分讨:
跟上面类似,有很多备选矩阵,不过这个是最常用的:
然后求逆:
类似地,可以发现这个矩阵对应的变换是高维后缀和。
\(\operatorname{xor}\)。稍微有点不一样:
常用的矩阵是:
求逆后:
这个就没啥特别的意义了。但能发现一个好玩的性质,对于任意二进制数 \(i,j\) 有:
其中 \(|i|\) 表示 \(i\) 的二进制表示下 \(1\) 的个数。
最后是代码,实现可以参考 \(\rm FFT\) 的实现:
const int w[3][2][2][2] =
{
{{{1, 0}, {1, 1}}, {{1, 0}, {mod - 1, 1}}},
{{{1, 1}, {0, 1}}, {{1, mod - 1}, {0, 1}}},
{{{1, 1}, {1, mod - 1}}, {{inv, inv}, {inv, mod - inv}}}
};
// typ: 0/1/2 或/且/异或
// on: 0/1 FWT/IFWT
// 注意这样实现在只需要用一种 FWT 的时候常数偏大
inline void FWT(int* f, int len, int typ, int on)
{
for (int h = 2, t = 1; h <= len; h <<= 1, t <<= 1)
for (int j = 0; j < len; j += h)
for (int k = j; k < j + t; ++k)
{
int u = f[k];
f[k] = ((ll)w[typ][on][0][0] * f[k] + (ll)w[typ][on][0][1] * f[k + t]) % mod;
f[k + t] = ((ll)w[typ][on][1][0] * u + (ll)w[typ][on][1][1] * f[k + t]) % mod;
}
}
例题有 CF662C Binary Table,CFgym103202M United in Stormwind,和不知道算不算的 P8292 [省选联考 2022] 卡牌 讲解可以看上一篇博客和我省选总结博客。
然后是关于 \(\operatorname{FWT}\) 比较好玩的一个性质,当序列 \(A\) 中非 \(0\) 项比较少时,直觉告诉我们求一遍 \(\operatorname{FWT}\) 浪费了很多计算,具体来讲,我们研究以下问题:
给出 \(n,m,k,w_{0\sim k-1},a_{1\sim n,0\sim k-1}\),求:
\[\prod_{i=1}^n\sum_{j=0}^{k-1}w_jz^{a_{i,j}} \]其中 \(\prod\) 表示异或卷积。\(0\le a_{i,j}<2^m\)。(\(1\le k\le 5,1\le n\le 10^5,1\le m\le 16\))
首先这里我们把刚刚一直在讨论的序列变成形式幂级数的形式了,但其实没啥区别,只是把 \(A_i\) 变成 \([z^i]A(z)\) 了。暴力 \(\operatorname{FWT}\) 的复杂度是 \(\mathcal{O}(nm2^m)\),显然不能接受。
考虑把 \(\operatorname{FWT}\) 之后的幂级数直接带进去:
则应该有:
现在我们的问题变为快速求出等式右边的式子,这样就能逐项确定最终答案的系数了。
根据我们刚刚发现的性质,这个玩意应该等于:
容易发现,根据 \(|p\operatorname{and}a_{i,l}|\) 的取值,和式一共能得到 \(2^k\) 种不同的值。这个种类不是很多,如果我们能想办法知道每种各出现了多少次,就能用快速幂很快求出整个积的值了。
首先给每种取值编个号,第 \(T\) 种取值为 \(s_T\),出现了 \(c_T\) 次,其中 \(T\) 为 \(k\) 位二进制数,且 \(w_l\) 取到负号,当且仅当 \(T_l=1\)。
枚举 \(0\sim k-1\) 的一个子集 \(T\)(可以为空),然后考虑这个和式:
记为 \(v_T\)。则可以发现一个等式:
原因比较简单,既取负数又被选在集合里的才会做出贡献,考虑等式右边 \(n\) 项中那 \(c_T\) 个项,考虑做出贡献的数是偶数个还是奇数个即可。因为它们做出的贡献全部相同,所以可以直接乘。
发现出现了好多 \(\bf F\) 形式的部分,我们先来看比较像 \(\operatorname{FWT}\) 形式的左边:
所以,如果我们能求出 \(v_U\),那再经过一次 \(\rm IFWT\) 就可以得到 \(c\),从而得到答案。
对于 \(v\),\(\prod\) 让这个式子变得扑朔迷离。考虑这样一个性质:
原因可以考虑对于每一位分讨,这里不再赘述。当然,它可以扩展到 \(\prod\) 的情况,从而我们可以把原式化为:
其中 \(\oplus\) 表示 \(\operatorname{xor}\) 运算。记 \(q_i=\bigoplus\limits_{l\in T}a_{i,l}\),接下来我们尝试把它化成 \(\operatorname{FWT}\) 的形式。
完成了。总结一下,我们首先要枚举 \(0\sim k-1\) 的子集,算出 \(cnt\)。然后 \(\operatorname{FWT}\) 得出 \(v\),\(\operatorname{IFWT}\) 一遍得出 \(c\),最后快速幂求出待求的幂级数的系数。最后 \(\operatorname{IFWT}\) 回来即可得到答案。时间复杂度 \(\mathcal{O}(n2^kk+(m+k)2^{m+k})\)。
实现可以参考我 这道题的提交,除了这道题还有两道弱化版 CF449D Jzzhu and Numbers 和 黎明前的巧克力。
子集卷积相关
考虑这样一个式子:
看起来很难入手。考虑这样一个性质:
联想刚刚我们用到的形式幂级数,发现第二个条件相当于朴素的多项式和卷积。所以考虑用二维形式幂级数来约束这个条件,具体来讲设 \(F(z,u)\) 为:
并定义子集卷积为:
而对应到最终的序列结果,如果设 \(h\) 序列是 \(f,g\) 子集卷积后的结果,那有:
我们考虑刚刚定义式的另一种理解方式,即把序列 \(f,g\) 进行普通的或卷积,\(i,j\) 会给第 \(i\operatorname{or}j\) 行第 \(|i|+|j|\) 列做贡献。而行列是独立的,所以我们又可以这样理解:
其中 \(\ast\) 表示或卷积。这样,只要提前 \(\operatorname{FWT}\) 好,就能把这个卷积变成点积,从而可以做到 \(\mathcal{O}(n\log^2n)\) 的复杂度,即枚举 \(|i|,|j|\),并做点积。
板子题的代码长这样:
for (int i = 0; i < n; ++i) scanf("%d", &a[pcnt(i)][i]);
for (int i = 0; i < n; ++i) scanf("%d", &b[pcnt(i)][i]);
for (int i = 0; i <= m; ++i) FWT(a[i], 1), FWT(b[i], 1);
for (int i = 0; i <= m; ++i)
for (int j = 0; j <= i; ++j)
for (int k = 0; k < n; ++k)
(c[i][k] += (ll)a[j][k] * b[i - j][k] % mod) %= mod;
for (int i = 0; i <= m; ++i) FWT(c[i], mod - 1);
for (int i = 0; i < n; ++i) printf("%d ", c[pcnt(i)][i]);
然后还有一道比较板的题,CF914G Sum the Fibonacci。选 \(s_a,s_b\) 的方案用子集卷积处理,选 \(s_d,s_e\) 的方案用异或卷积处理,最后再用一次与卷积复合即可。注意选的方案数乘上它的值对应的斐波那契数即为它在最后的与卷积的系数。
当然,这里对于第二维 \(u\),我们可以做许多常见的多项式操作,比如半在线卷积,多项式 \(\exp\),多项式 \(\ln\),多项式求逆等。这些一般我们都会采用暴力的写法。(全家桶那种用大常数,码量,模数限制把 \(\mathcal{O}(\log^2n)\) 变成 \(\mathcal{O}(\log n\log\log n)\) 的方法大部分情况是用不到了)我曾经记录过,忘了可以 去看看,其实推也挺好推的。
对了,这里放一个 \(\exp\) 和 \(\ln\) 的定义式,\(\ln\) 的我想不明白是为啥,但好像还挺有用:
半在线卷积可以看 P4221 [WC2018]州区划分。因为不算太难而且上一篇讲过了就不说了,而且半在线卷积和普通卷积的暴力感觉差不了多少。
然后是一道 \(\exp\) 的题但能用枚举子集过的 P6570 [NOI Online #3 提高组] 优秀子序列。发现两两没交集,最后求和,其实就是子集卷积的形式。再加上选子序列这种很像背包的限制,所以可以列出式子:
其中 \(\prod\) 表示子集卷积。(\(u\) 只在推导里用一下,知道具体方法之后大概就不会把它拿出来用了)这样我们就能知道选出集合 \(S\) 的方案数 \(p_S\),从而可以得到它的贡献为 \(p_S\varphi(1+S)\)。不过这一套对 \(a_i=0\) 的项不管用。不过没事,因为它们可以随便挤进任何一个选择方案里,如果设有 \(cnt\) 个 \(a_i=0\) 的项,则最后所有的 \(p_S\) 都乘上 \(2^{cnt}\) 即可。
当然,这个形式直接做卷积是不可取的。不过在处理普通形式幂级数做的背包时,我们用过求 \(\ln\) 的技巧,那就套过来试试吧:
看起来化简不下去了。但是!不要忘了我们的乘法现在定义的是子集卷积!所以 \(k>1\) 时,\((z^{a_i})^k\) 这一项会变为 \(0\),因为没法选出两个没有交集的非 \(0\) 项了。这样,我们就能大幅化简上面的式子变为:
直接做 \(\exp\) 即可。时间复杂度 \(\mathcal{O}(n+a_i\log^2a_i)\)。评测记录。
除了优化背包,常见的组合意义也能在这里用上。比如这道题 #94. 【集训队互测2015】胡策的统计。如果设 \([z^S]F(z)\) 表示点集 \(S\) 能形成的连通子图个数,则枚举连通块个数我们可以得到答案:
其中乘法还是子集卷积。原因比较好想,就是人为把图分成若干不相关的连通块。所以现在的问题在于求出连通子图个数。
我们发现,求任意子图的个数是好求的,如果设点集 \(S\) 内部边有 \(c_S\) 条,则可以任意子图的个数为 \([z^S]G(z)=2^{c_S}\)。可以发现,任意子图是由一些连通子图组成的,所以应该有:
从而有:
求个 \(\ln\) 再求个逆即可。注意因为 \(\operatorname{FWT}\) 是线性变换,所以 \(F(z)\) 变成 \(1-F(z)\) 的操作不需要 \(\operatorname{IFWT}\) 回来再做,直接变即可。常数很大的代码。
挖个坑,还没调对的 \(\rm kexp\),#154. 集合划分计数。
差不多就写这么多吧。参考学习了神 \(\tt command\_block\) 的文章。