update:(binary-set.md):更新二进制集合操作 (#4285)

* 更新二进制集合操作

* Update binary-set.md

* style: format markdown files with remark-lint

* syntax error

unintelligent bot

* Update docs/math/binary-set.md

Co-authored-by: Tifa <62847935+Tiphereth-A@users.noreply.github.com>

* Update .remarkignore

Co-authored-by: 24OI-bot <15963390+24OI-bot@users.noreply.github.com>
Co-authored-by: aofall <aofall@users.noreply.github.com>
Co-authored-by: Tifa <62847935+Tiphereth-A@users.noreply.github.com>
Co-authored-by: Dejia Xu <dejia@utexas.edu>
pull/4305/head
Great-designer 2022-09-16 01:06:04 +08:00 committed by GitHub
parent 95ef1f557d
commit 86fc82f4da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 49 deletions

View File

@ -3,3 +3,4 @@ pas-cpp.md
bit.md
generator.md
java-pro.md
binary-set.md

141
docs/math/binary-set.md Normal file
View File

@ -0,0 +1,141 @@
## 二进制集合操作
一个数的二进制表示可以看作是一个集合($0$ 表示不在集合中,$1$ 表示在集合中)。比如集合 $\{1,3,4,8\}$,可以表示成 $(100011010)_2$。而对应的位运算也就可以看作是对集合进行的操作。
| 操作 | 集合表示 | 位运算语句 |
| ------ | :-------------: | :-------------------------: |
| 交集 | $a \cap b$ | `a & b` |
| 并集 | $a \cup b$ | <code>a &#124; b</code> |
| 补集 | $\bar{a}$ | `~a` (全集为二进制都是 1 |
| 差集 | $a \setminus b$ | `a & (~b)` |
| 对称差 | $a\triangle b$ | `a ^ b` |
在进一步介绍集合的子集遍历操作之前,先看位运算的有关应用例子。
### 模 2 的幂
一个数对 $2$ 的非负整数次幂取模,等价于取二进制下一个数的后若干位,等价于和 $mod-1$ 进行与操作。
```cpp
// C++ Version
int modPowerOfTwo(int x, int mod) { return x & (mod - 1); }
```
```python
# Python Version
def modPowerOfTwo(x, mod):
return x & (mod - 1)
```
于是可以知道,$2$ 的非负整数次幂对它本身取模,结果为 $0$,即如果 $n$ 是 $2$ 的非负整数次幂,$n$ 和 $n-1$ 的与操作结果为 $0$。
事实上,对于一个正整数 $n$$n-1$ 会将 $n$ 的最低 $1$ 位置零,并将后续位数全部置 $1$。因此,$n$ 和 $n-1$ 的与操作等价于删掉 $n$ 的最低 $1$ 位。
借此可以判断一个数是不是 $2$ 的非负整数次幂。当且仅当 $n$ 的二进制表示只有一个 $1$ 时,$n$ 为 $2$ 的非负整数次幂。
```cpp
// C++ Version
bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
```
```python
# Python Version
def isPowerOfTwo(n):
return n > 0 and (n & (n - 1)) == 0
```
### 子集遍历
遍历一个二进制数表示的集合的全部子集,等价于枚举二进制数对应掩码的所有子掩码。
掩码是一串二进制码,用于和源码进行与运算,得到屏蔽源码的若干输入位后的新操作数。
掩码对于源码可以起到遮罩的作用,掩码中的 $1$ 位意味着源码的相应位得到保留,掩码中的 $0$ 位意味着源码的相应位进行置 $0$ 操作。将掩码的若干 $1$ 位改为 $0$ 位可以得到掩码的子掩码,掩码本身也是自己的子掩码。
给定一个掩码 $m$,希望有效迭代 $m$ 的所有子掩码 $s$,可以考虑基于位运算技巧的实现。
```cpp
// 降序遍历 m 的非空子集
int s = m;
while (s > 0) {
// s 是 m 的一个非空子集
s = (s - 1) & m;
}
```
或者使用更紧凑的 for 语句:
```cpp
// 降序遍历 m 的非空子集
for (int s = m; s; s = (s - 1) & m)
// s 是 m 的一个非空子集
```
这两段代码都不会处理等于 $0$ 的子掩码,要想处理等于 $0$ 的子掩码可以使用其他办法,例如:
```cpp
// 降序遍历 m 的子集
for (int s = m;; s = (s - 1) & m) {
// s 是 m 的一个子集
if (s == 0) break;
}
```
接下来证明,上面的代码访问了所有 $m$ 的子掩码,没有重复,并且按降序排列。
假设有一个当前位掩码 $s$,并且想继续访问下一个位掩码。在掩码 $s$ 中减去 $1$,等价于删除掩码 $s$ 中最右边的设置位,并将其右边的所有位变为 $1$。
为了使 $s-1$ 变为新的子掩码,需要删除掩码 $m$ 中未包含的所有额外的 $1$ 位,可以使用位运算 $(s-1)\&m$ 来进行此移除。
这两步操作等价于切割掩码 $s-1$,以确定算术上可以取到的最大值,即按降序排列的 $s$ 之后的下一个子掩码。
因此,该算法按降序生成该掩码的所有子掩码,每次迭代仅执行两个操作。
特殊情况是 $s=0$。在执行 $s-1$ 之后得到 $-1$,其中所有位都为 $1$。在 $(s-1)\&m$ 操作之后将得到新的 $s$ 等于 $m$。因此,如果循环不以 $s=0$ 结束,算法的循环将无法终止。
使用 $\text{popcount}(m)$ 表示 $m$ 二进制中 $1$ 的个数,用这种方法可以在 $O(2^{\text{popcount}(m)})$ 的时间复杂度内遍历集合 $m$ 的子集。
### 遍历所有掩码的子掩码
在使用掩码动态编程的问题中,有时会希望对于每个掩码,遍历掩码的所有子掩码:
```cpp
for (int m = 0; m < (1 << n); ++m)
// 降序遍历 m 的非空子集
for (int s = m; s; s = (s - 1) & m)
// s 是 m 的一个非空子集
```
这样做可以遍历大小为 $n$ 的集合的每个子集的子集。
接下来证明,该操作的时间复杂度为 $O(3^n)$$n$ 为掩码总共的位数,即集合中元素的总数。
考虑第 $i$ 位,即集合中第 $i$ 个元素,有三种情况:
- 在掩码 $m$ 中为 $0$,因此在子掩码 $s$ 中为 $0$,即元素不在大小子集中。
- 在 $m$ 中为 $1$,但在 $s$ 中为 $0$,即元素只在大子集中,不在小子集中。
- 在 $m$ 和 $s$ 中均为 $1$,即元素同时在大小子集中。
总共有 $n$ 位,因此有 $3^n$ 个不同的组合。
还有一种证明方法是:
如果掩码 $m$ 具有 $k$ 个 $1$,那么它有 $2^k$ 个子掩码。对于给定的 $k$,对应有 $C_n^k$ 个掩码 $m$,那么所有掩码的总数为:
$$
\sum_{k=0}^n C_n^k 2^n
$$
上面的和等于使用二项式定理对 $(1+2)^n$ 的展开,因此有 $3^n$ 个不同的组合。
### 参考资料
**本页面主要译自博文 [Перебор всех подмасок данной маски](http://e-maxx.ru/algo/all_submasks) 与其英文翻译版 [Submask Enumeration](https://cp-algorithms.com/algebra/all-submasks.html)。其中俄文版版权协议为 Public Domain + Leave a Link英文版版权协议为 CC-BY-SA 4.0。**
### 习题
- [Atcoder - Close Group](https://atcoder.jp/contests/abc187/tasks/abc187_f)
- [Codeforces - Nuclear Fusion](http://codeforces.com/problemset/problem/71/E)
- [Codeforces - Sandy and Nuts](http://codeforces.com/problemset/problem/599/E)
- [Uva 1439 - Exclusive Access 2](https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=4185)
- [UVa 11825 - Hackers' Crackdown](https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2925)

View File

@ -125,32 +125,6 @@ def divPowerOfTwo(n, m): # 计算 n/(2^m)
!!! warning
我们平常写的除法是向 $0$ 取整,而这里的右移是向下取整(注意这里的区别),即当数大于等于 $0$ 时两种方法等价,当数小于 $0$ 时会有区别,如: `-1 / 2` 的值为 $0$ ,而 `-1 >> 1` 的值为 $-1$ 。
判断一个数是不是 $2$ 的非负整数次幂:
```cpp
// C++ Version
bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
```
```python
# Python Version
def isPowerOfTwo(n):
return n > 0 and (n & (n - 1)) == 0
```
对 $2$ 的非负整数次幂取模:
```cpp
// C++ Version
int modPowerOfTwo(int x, int mod) { return x & (mod - 1); }
```
```python
# Python Version
def modPowerOfTwo(x, mod):
return x & (mod - 1)
```
### 取绝对值
在某些机器上,效率比 `n > 0 ? n : -n` 高。
@ -290,29 +264,6 @@ def flapBit(a, b):
这些操作相当于将一个 $32$ 位整型变量当作一个长度为 $32$ 的布尔数组。
### 模拟集合操作
一个数的二进制表示可以看作是一个集合($0$ 表示不在集合中,$1$ 表示在集合中)。比如集合 $\{1,3,4,8\}$ ,可以表示成 $(100011010)_2$ 。而对应的位运算也就可以看作是对集合进行的操作。
| 操作 | 集合表示 | 位运算语句 |
| ------ | :-------------: | :-------------------------: |
| 交集 | $a \cap b$ | `a & b` |
| 并集 | $a \cup b$ | `a|b` |
| 补集 | $\bar{a}$ | `~a` (全集为二进制都是 1 |
| 差集 | $a \setminus b$ | `a & (~b)` |
| 对称差 | $a\triangle b$ | `a ^ b` |
子集遍历:
```cpp
// 遍历 u 的非空子集
for (int s = u; s; s = (s - 1) & u) {
// s 是 u 的一个非空子集
}
```
用这种方法可以在 $O(2^{\text{popcount}(u)})$ $\text{popcount}(u)$ 表示 $u$ 二进制中 1 的个数)的时间复杂度内遍历 $u$ 的子集,进而可以在 $O(3^n)$ 的时间复杂度内遍历大小为 $n$ 的集合的每个子集的子集。(复杂度为 $O(3^n)$ 是因为每个元素都有 不在大子集中/只在大子集中/同时在大小子集中 三种状态。)
## 汉明权重
汉明权重是一串符号中不同于定义在其所使用的字符集上的零符号zero-symbol的个数。对于一个二进制数它的汉明权重就等于它 $1$ 的个数(即 `popcount`)。

View File

@ -213,6 +213,7 @@ nav:
- 符号: math/notation.md
- 进位制: math/base.md
- 位运算: math/bit.md
- 二进制集合操作: math/binary-set.md
- 平衡三进制: math/balanced-ternary.md
- 高精度计算: math/bignum.md
- 快速幂: math/binary-exponentiation.md