参考:
〇 引入
题意 起初有一个数字0,每一秒钟随机选择一个数字与手上的数字作按位或。选择数字i 的概率是pi。问期望多少秒后,手上的数字变成2n−1。( 洛谷 P3175 [HAOI2015] 按位或)
分析 二进制n 位数字每一位都要变成 1,先分开考虑,求每一位变成 1 的期望时间,然后再合并。但每种情形并不独立,相加后需要减去一些,有点容斥原理的感觉。
考虑 Min-Max 反演的期望形式。
设xi 表示第i 个二进制位变为 1 的期望时间,S 是所有xi 的集合,那么E[min(S)] 表示某个集合T 中 有一个 位置变成 1 的期望时间,E[min(S)] 表示 所有 位置都变成 1 的期望时间。(这里的定义不太符合数学规范,但不影响理解)
所求是E[max(S)],转化为E[min(T)] 求解。这样有一个位置变成 1,也即变成 1 这个事件第一次发生,就可以套用几何分布,E[min(T)]=∑A∩T=∅pA1=1−∑A⊂TpA1。
下面介绍如何求解形如FS=A⊂S∑f(A) 这种 下标是子集关系的求和问题。
一 二进制表示与集合
一个数的二进制表示可以看作是一个集合,二进制第i 位对应集合的第i 个元素,0 表示不在集合中,1 表示在集合中。对应的位运算也可看作是对集合的操作,如
空集∅ 对应于 0
,全集对应于 -1
,即二进制都是 1;
交集a∩b ——按位与 a & b
;
并集a∪b ——按位或 a | b
;
补集a ——按位取反 ~a
或 a ^ ((1 << n) - 1)
;
差集a∖b —— a & (~b)
;
对称差aΔb ——按位异或 a ^ b
;
t 是s 的子集t⊂s —— (t | s) == s
;
t 是s 的超集t⊃s —— (t & s) == s
;
s 的元素个数∣s∣ —— __builtin_popcountll(s)
或 s.count()
;
__builtin_ctzll
:末尾的 0,即对 lowbit 取 __lg
;
__builtin_clzll
:开头的 0,用 31 减是 __lg
。(复杂度都是O(1),如果是 long long,函数名末尾加 ll,31 改成 63)
二 子集和超集和
子集和(高维前缀和)
FS=A⊂S∑f(A) 转化为位运算语言即Fs=t∣s=s∑ft。
暴力状压 DP 枚举子集,时间复杂度是O(3n)。【n=17 时O(3n)=O(108)】
1 2 3 4 5 6
| for (int s = 0; s < 1 << n; s++) { for (int t = s; t; t = (t - 1) & s) { sum[s] += a[t]; } sum[s] += a[0]; }
|
优化此过程,设fi,s 表示只有最低i 位与s 不同的 mask 之和,复杂度为O(n⋅2n)。【n=22 时O(n⋅2n)=O(108)】
1 2 3 4 5 6 7 8 9
| for (int i = 0; i < n; i++) { for (int j = 0; j < 1 << n; j++) { if (j >> i & 1) { f[j][i] = f[j][i - 1] + f[j ^ (1 << i)][i - 1]; } else { f[j][i] = f[j][i - 1]; } } }
|
滚动数组为:
1 2 3 4 5 6 7
| for (int i = 0; i < n; i++) { for (int j = 0; j < 1 << n; j++) { if (j >> i & 1) { f[j] += f[j ^ (1 << i)]; } } }
|
这就是最经典的 SOS DP,下面是一些变式。
子集和反演(高维差分)
如果
Fs=t∣s=s∑ft⟷FS=T⊆S∑fT
那么反演式为
ft=s∣t=t∑(−1)∣t∣−∣s∣Fs⟷fT=S⊆T∑(−1)∣T∣−∣S∣FS
将上面代码加号改成减号,就是做反演。
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
| template <typename T> void FMTOR(vector<T>& a, int o) { const int n = __lg(a.size()); for (int i = 0; i < n; i++) { for (int j = 0; j < 1 << n; j++) { if (j >> i & 1) { a[j] += o * a[j ^ (1 << i)]; } } } }
template <typename T> void FWTOR(vector<T>& a, int o) { const int n = a.size(); 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] += o * a[i + j]; } } } }
|
上面两段代码也分别称作莫比乌斯 (Mobius) 变换和沃尔什 (Walsh) 变换。
超集和(高维后缀和与差分)
将每个数按位取反,就能得到超集和:
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
| template <typename T> void FMTAND(vector<T>& a, int o) { const int n = __lg(a.size()); for (int i = 0; i < n; i++) { for (int j = 0; j < 1 << n; j++) { if (j >> i & 1) { a[j ^ (1 << i)] += o * a[j]; } } } }
template <typename T> void FWTAND(vector<T>& a, int o) { const int n = a.size(); 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] += o * a[i + j + mid]; } } } }
|
三 位卷积
或卷积
在 [[…/ 数学 / 组合计数 | 上篇文章]] 中,你已经知道ak=i+j=k∑figj 这种式子可以用卷积结合 FFT 或 NTT 快速求解。如果指标关系不是i+j=k,而是i∣j=k,就需要用到子集和变换及其反演。
将两个序列f,g 的子集和相乘后做子集和反演,得到 或卷积ak=i∣j=k∑figj。证明:
As=t∣s=s∑at=t∣s=s∑i∣j=t∑figj=i∣j∣s=s∑figj=i∣s=s∑fij∣s=s∑gj=FsGs
1 2 3 4 5 6 7 8 9 10 11
| template <typename T> vector<T> operator | (vector<T> a, vector<T> b) { const int n = a.size(); FWTOR(a, 1); FWTOR(b, 1); for (int i = 0; i < n; i++) { a[i] *= b[i]; } FWTOR(a, -1); return a; }
|
与卷积
将两个序列f,g 的超集和相乘后做超集和反演,得到 与卷积ak=i&j=k∑figj。
1 2 3 4 5 6 7 8 9 10 11
| template <typename T> vector<T> operator & (vector<T> a, vector<T> b) { const int n = a.size(); FWTAND(a, 1); FWTAND(b, 1); for (int i = 0; i < n; i++) { a[i] *= b[i]; } FWTAND(a, -1); return a; }
|
异或卷积
异或比较复杂,设x∘y 表示x 与y 二进制中公共 1 的数量,那么调用按位异或变换将得到Ai=j∘i=0∑aj−j∘i=1∑aj,目前感觉没什么用;将两个序列f,g 在平行时空相乘后做反变换,得到 异或卷积ak=i⊕j=k∑figj。
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
| template <typename T, typename U> void FWTXOR(vector<T>& a, U o) { const int n = a.size(); for (int mid = 1; mid < n; mid <<= 1) { for (int i = 0; i < n; i += mid << 1) { for (int j = 0; j < mid; j++) { auto& x = a[i + j]; auto& y = a[i + j + mid]; tie(x, y) = pair(o * (x + y), o * (x - y)); } } } }
template <typename T> vector<T> operator ^ (vector<T> a, vector<T> b) { const int n = a.size(); FWTXOR(a, 1); FWTXOR(b, 1); for (int i = 0; i < n; i++) { a[i] *= b[i]; } FWTXOR(a, Z(1) / 2); return a; }
|
子集卷积 × 一般 DP 的设计思路
子集卷积 定义为ak=i&j=0,i∣j=k∑figj。
处理下标两种限制的卷积太过麻烦,不妨扩维,设
fp,i′={0,fi,∣i∣=p∣i∣=p
gq,j′ 同理,我们求ar,k′,
ar,k′=p+q=r,i&j=0,i∣j=k∑fp,i′gq,j′=p+q=r∑i∣j=k∑fp,i′gq,j′,
现在不需要考虑限制条件i&j=0,将下标的两种限制转化为两种下标的限制。
i∣j=k∑fp,i′gq,j′ 是fp′ 与gq′ 的或卷积,先枚举外层循环,内层做或卷积即可。时间复杂度O(n2⋅2n),n=18 时O(n2⋅2n)=O(108),比O(3n) 的方法略快。
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
| cin >> n; vector f(n + 1, vector<Z>(1 << n)); vector g(n + 1, vector<Z>(1 << n)); vector a(n + 1, vector<Z>(1 << n)); for (int i = 0; i < (1 << n); i++) { cin >> f[__builtin_popcount(i)][i]; } for (int i = 0; i < (1 << n); i++) { cin >> g[__builtin_popcount(i)][i]; } for (int i = 0; i <= n; i++) { FMTOR(f[i], 1); FMTOR(g[i], 1); } for (int r = 0; r <= n; r++) { for (int p = 0; p <= r; p++) { for (int s = 0; s < 1 << n; s++) { a[r][s] += f[p][s] * g[r - p][s]; } } } for (int i = 0; i <= n; i++) { FMTOR(a[i], -1); } for (int i = 0; i < (1 << n); i++) { cout << a[__builtin_popcount(i)][i] << " "; } cout << endl;
|
四
数论中的莫比乌斯变换
在数论中,莫比乌斯变换是这种形式
f(n)=d∣n∑g(d)⟺g(n)=d∣n∑μ(d)g(dn)
其他的变换可以看 狄利克雷卷积和莫比乌斯反演。
如果我们传入的参数 n 是一个 Square Free Number,即没有平方因子的数,这种数由于仅存在 0/1 的指数,所以可以用二进制表示, g 函数是一个恰好仅与传入参数有关的式子,那么我们就能将上式改写成:
f(S)=T⊆S∑g(T)⟺g(T)=S⊆T∑(−1)∣T∖S∣f(S)
本质上就是广义莫比乌斯变换的一种特殊形式。