有点重要的知识点,不记得省选考不考了。
1. 定义
一个长度为 $2 ^ n$ 的序列,有一个生成函数为 $F(x) = \sum_{i = 0} ^ {2 ^ n - 1}f(i) x ^ i$,这个生成函数就叫集合幂级数。
为什么叫集合幂级数呢?是因为我们可以把 $i$ 看作一个 $\{0, 1, \cdots, n - 1\}$ 的子集并状态压缩。
定义以下几个计算:
- 或卷积:$c(i) = \sum_{j \mid k = i}a(j)b(k)$。
- 与卷积:$c(i) = \sum_{j\odot k = i}a(j)b(k)$。
- 异或卷积:$c(i) = \sum_{j\oplus k = i}a(j)b(k)$。
- 子集卷积:$c(i) = \sum_{j\odot k = 0, j \mid k = i}b(j)c(k)$。
还有一些奇怪的科技,如子集 ln,子集 exp,这里先鸽了。
这些东西如果是暴力计算的话,显然是 $O(4 ^ n)$ 的,我们考虑有没有更优的做法。
2. FMT / FWT
1)FMT
定义高维前缀和为:$b(i) = \sum_{j\subseteq i}a(j)$。容易发现暴力做是 $O(3 ^ n)$ 的。
为什么叫高位前缀和呢?因为他相当于是 $n$ 维数组做前缀和,每维只有 01 两种。(所以好像就有了 $k$ 进制下的高维前缀和)
考虑如果只有 2 维,怎么做呢?
显然枚举每一维,再对该维前缀和,代码如下:
1 | for (int i = 0; i < n; ++ i) |
显然是枚举了两维,虽然和平常写法不同,但是如果手玩一下发现是对的。
扩展到 $n$ 维,我们就可以得到一个简短的代码:
1 | for (int i = 0; i < n; ++ i) |
这个代码显然是 $O(n2 ^ n)$ 的。
同理,我们也可以得到高位后缀和的代码:
1 | for (int i = 0; i < n; ++ i) |
高维前缀和和高维后缀和统称为快速莫比乌斯变换,简写为 FMT。
2)FWT
定义 $\displaystyle \text{FWT}(A) = \sum_{i = 0} ^ {2 ^ n - 1}x ^ i\sum_{j = 0} ^ {2 ^ n - 1}(-1) ^ {|i\odot j|}[x ^ j]A(x)$,这个东西就叫快速沃尔什变换,简写为 FWT。显然暴力计算是 $O(4 ^ n)$ 的。
类似于 FMT,我们逐维考虑。枚举每一维 $i$,假设我们枚举到 $j$ 不包含 $i$ 这一维。那么对于不包含 $i$ 的 $j$,显然会将两个都加起来,因为自己是 0,不管与谁都是 0,即 $a_j := a_j + a_{j\mid 2^i}, $,而包含 $i$ 的 $j\mid2^i$ 就不同了,如果是 $a_{j\mid 2 ^ i}$,显然贡献到 $a_{j\mid 2 ^ i}$ 就是 -1 了,即 $a_{j\mid2 ^ i}:= a_j - a_{j\mid 2 ^ i}$。
我们也可以写出代码:
1 | for (int i = 0; i < n; ++ i) |
我们还可以写出另一份代码:
1 | int bit, tot = 1 << bit; |
发现和 NTT 其实是很像的。其实集合幂级数和多项式是有很多相似点的,我们这里才刚刚看到。
显然,时间复杂度为 $O(n2 ^ n)$。
3. 实现卷积
1)与卷积
考虑证明假设 $C(x) = \sum_{i = 0} ^ {2 ^ n - 1}x ^ i \sum_{j\odot k = i} [x ^ j]A(x) \times [x ^ k]B(x)$,那么对 $A, B, C$ 高位前缀和后,$C(x) = \sum_{i = 0} ^ {2 ^ n - 1}x ^ i[x ^ i]A(x) \times [x ^ i]B(x)$。
考虑每一对 $j, k$,最后 $j, k$ 一定会贡献到 $j\odot k$ 的所有子集,即 $\forall x\subseteq (j\odot k)$。而 $C$ 贡献显然也是 $\forall x\subseteq i$,这和前面的贡献是相同的。
于是我们先对 $A, B$ 高位前缀和,乘起来,再做高位前缀和的逆变换就可以得到 $C$ 了。
逆变换就将加号变为减号即可。
1 | for (int i = 0; i < n; ++ i) |
2)与卷积
考虑类似于或卷积的方法,这里不再赘述,证明留给读者。
3)异或卷积
前面已经定义了 $FWT(A)$,类似于 FMT,我们证明 $FWT(C) = FWT(A) * FWT(b)$。
还是考虑 $i, j, p$ 的贡献,$i\to p$ 的贡献显然是 $a(i)(-1) ^ {\mid p\odot i\mid }$,$j\to p$ 的贡献显然是 $b(j)(-1) ^ {\mid p\odot j\mid }$。
考虑 $i\oplus j$ 对 $p$ 的贡献,显然是 $(-1) ^ {(i\oplus j)\odot p}$。所以我们考虑证明 $(-1) ^ {\mid (i\oplus j)\odot p\mid } = (-1) ^ {\mid i\odot p\mid }(-1) ^ {\mid j\odot p\mid }$。
容易发现 $p$ 的限制就是将 $i, j$ 中 $p$ 该位为 1 的位拿出来,其余的不管,我们可以先对 $i$ 赋值为 $i\odot p$,$j$ 同理,显然最后的答案左边就是 $(-1) ^ {\mid i\oplus j\mid}$,容易发现就是 $i$ 的 1 个数和 $j$ 的 1 个数相加。左右显然相等。
逆变换还是将操作反过来即可。这里给出一种实现。
1 | int tot = 1 << n; |
另外,可以像 NTT 一样实现。
1 | int tot = 1 << n; |
4)子集卷积
先不考虑 $j\odot k = 0$ 的情况,容易发现这就是一个简单的或卷积。但是这个条件似乎不好处理。
发现当 $j, k\to j| k$ 是有贡献的,当且仅当 $\mid j\mid + \mid k\mid = \mid j|k\mid$,也就是说,我们限制的集合的大小。这启示我们按照集合大小分类。
我们考虑计算集合大小分别为 $a$ 和 $b$ 的或卷积,贡献到集合大小为 $a + b$ 的位置上。这样暴力做是 $O(n ^ 3 2 ^ n)$ 的,但是发现中间对 $a, b$ 的或卷积是由重复的,我们可以先将 $a = 0, \dots n - 1, b = 0, \dots n - 1$ 的高维前缀和后,再批量贡献到 $a + b$,再将 $a + b = 0, \dots, n - 1$ 的高维前缀和逆变换。容易发现时间复杂度是 $O(n ^ 2 2 ^ n)$ 的。代码在后面的例题。
4. 例题
T1:模板
1 | void base_or(int *a, int inv) |
1 | int main() |
T2:Sum the Fibonacci
也是模板,请读者自行实现。