有点重要的知识点,不记得省选考不考了。
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 2 3 4 for (int i = 0 ; i < n; ++ i) for (int j = 1 ; j < n; ++ j) a[i][j] += a[i][j - 1 ]; for (int i = 1 ; i < n; ++ i) for (int j = 0 ; j < n; ++ j) a[i][j] += a[i - 1 ][j];
显然是枚举了两维,虽然和平常写法不同,但是如果手玩一下发现是对的。
扩展到 $n$ 维,我们就可以得到一个简短的代码:
1 2 3 for (int i = 0 ; i < n; ++ i) for (int j = 0 ; j < (1 << n); ++ j) if (j >> i & 1 ) a[j] += a[j ^ (1 << i)];
这个代码显然是 $O(n2 ^ n)$ 的。
同理,我们也可以得到高位后缀和的代码:
1 2 3 for (int i = 0 ; i < n; ++ i) for (int j = 0 ; j < (1 << n); ++ j) if (j >> i & 1 ) a[j ^ (1 << i)] += a[j];
高维前缀和和高维后缀和统称为快速莫比乌斯变换 ,简写为 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 2 3 4 5 6 for (int i = 0 ; i < n; ++ i) for (int j = 0 ; j < (1 << n); ++ j) if (j >> i & 1 ) { int x = a[j], y = a[j | (1 << i)]; a[j] = x + y, a[j | (1 << i)] = x - y; }
我们还可以写出另一份代码:
1 2 3 4 5 6 7 int bit, tot = 1 << bit;for (int mid = 1 ; mid < tot; mid <<= 1 ) for (int i = 0 ; i < tot; i += (mid << 1 )) for (int j = 0 ; j < mid; ++ j) { int x = a[i + j], y = a[i + j + mid]; a[i + j] = x + y, a[i + j + mid] = x - y; }
发现和 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 2 3 for (int i = 0 ; i < n; ++ i) for (int j = 0 ; j < (1 << n); ++ j) if (j >> i & 1 ) a[j] -= a[j ^ (1 << 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 2 3 4 5 6 7 int tot = 1 << n;for (int mid = 1 ; mid < tot; mid <<= 1 ) for (int i = 0 ; i < tot; i += (mid << 1 )) for (int j = 0 ; j < mid; ++ j) { int x = a[i + j], y = a[i + j + mid]; a[i + j] = (x + y) >> 1 , a[i + j + mid] = (x - y) >> 1 ; }
另外,可以像 NTT 一样实现。
1 2 3 4 5 6 7 8 int tot = 1 << n;for (int mid = 1 ; mid < tot; mid <<= 1 ) for (int i = 0 ; i < tot; i += (mid << 1 )) for (int j = 0 ; j < mid; ++ j) { int x = a[i + j], y = a[i + j + mid]; a[i + j] = x + y, a[i + j + mid] = x - y; } for (int i = 0 ; i < tot; ++ i) a[i] >>= 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:模板 FMT / FWT 模板
子集卷积模板 lg ,子集卷积模板 LOJ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 void base_or (int *a, int inv) { for (int i = 0 ; i < bit; ++ i) for (int j = 0 ; j < tot; ++ j) if (j >> i & 1 ) (a[j] += a[j ^ (1 << i)] * inv) %= Mod; for (int j = 0 ; j < tot; ++ j) a[j] = (a[j] + Mod) % Mod; } void get_or (int *A, int *B) { memcpy (a, A, sizeof (int ) * tot), memcpy (b, B, sizeof (int ) * tot); base_or (a, 1 ), base_or (b, 1 ); for (int i = 0 ; i < tot; ++ i) c[i] = (LL)a[i] * b[i] % Mod; base_or (c, -1 ); for (int i = 0 ; i < tot; ++ i) printf ("%d " , c[i]); puts ("" ); } void base_and (int *a, int inv) { for (int i = 0 ; i < bit; ++ i) for (int j = tot - 1 ; j; -- j) if (j >> i & 1 ) (a[j ^ (1 << i)] += a[j] * inv) %= Mod; for (int j = 0 ; j < tot; ++ j) a[j] = (a[j] + Mod) % Mod; } void get_and (int *A, int *B) { memcpy (a, A, sizeof (int ) * tot), memcpy (b, B, sizeof (int ) * tot); base_and (a, 1 ), base_and (b, 1 ); for (int i = 0 ; i < tot; ++ i) c[i] = (LL)a[i] * b[i] % Mod; base_and (c, -1 ); for (int i = 0 ; i < tot; ++ i) printf ("%d " , c[i]); puts ("" ); } void base_xor (int *a, int inv) { if (inv == -1 ) inv = inv2; for (int i = 0 ; i < bit; ++ i) for (int j = 0 ; j < tot; ++ j) { if (j >> i & 1 ) continue ; LL x = a[j], y = a[j | (1 << i)]; a[j] = (x + y) % Mod * inv % Mod; a[j | (1 << i)] = (x - y + Mod) % Mod * inv % Mod; } } void get_xor (int *A, int *B) { memcpy (a, A, sizeof (int ) * tot), memcpy (b, B, sizeof (int ) * tot); base_xor (a, 1 ), base_xor (b, 1 ); for (int i = 0 ; i < tot; ++ i) c[i] = (LL)a[i] * b[i] % Mod; base_xor (c, -1 ); for (int i = 0 ; i < tot; ++ i) printf ("%d " , c[i]); puts ("" ); }
1 2 3 4 5 6 7 8 9 10 int main () { for (int i = 0 ; i <= bit; ++ i) FWT (a[i], bit, 1 ), FWT (b[i], bit, 1 ); for (int i = 0 ; i <= bit; ++ i) for (int j = 0 ; j <= bit - i; ++ j) for (int k = 0 ; k < (1 << bit); ++ k) c[i + j][k] = (c[i + j][k] + (LL)a[i][k] * b[j][k]) % Mod; for (int i = 0 ; i <= bit; ++ i) FWT (c[i], bit, -1 ); return 0 ; }
T2:Sum the Fibonacci 题目传送门 Codeforces
也是模板,请读者自行实现。