本文转载自力扣讨论区,作者:灵茶山艾府(endlesscheng)。 原文链接:https://leetcode.cn/circle/discuss/CaOJ45/

图:集合交、按位与之间存在某种联系。
前言
本文将扫清位运算的迷雾,在集合论与位运算之间建立一座桥梁。
在高中,我们学了集合论(set theory)的相关知识。例如,包含若干整数的集合 HashSet,C++ 中的 std::unordered_set。
在集合论中,有交集
该二进制登场了。
集合可以用二进制表示,二进制从低到高第
正式地说,包含非负整数的集合
例如集合
利用位运算「并行计算」的特点,我们可以高效地做一些和集合有关的运算。按照常见的应用场景,可以分为以下四类:
- 集合与集合
- 集合与元素
- 遍历集合
- 枚举集合
一、集合与集合
其中
两个集合的「对称差」是只属于其中一个集合,而不属于另一个集合的元素组成的集合,也就是不在交集中的元素组成的集合。
| 术语 | 集合 | 位运算 | 集合示例 | 位运算示例 |
|---|---|---|---|---|
| 交集 | ||||
| 并集 | ||||
| 对称差 | ||||
| 差 | ||||
| 差(子集) | ||||
| 包含于 |
注 1:按位取反的例子中,仅列出最低
个比特位取反后的结果,即 取反后是 。 注 2:包含于(判断子集)的两种位运算写法是等价的,在编程时只需判断其中任意一种。此外,还可以用
(a & ~b) == 0判断,如果成立,也表示是 的子集。 注 3:编程时,请注意运算符的优先级。例如
==在某些语言中优先级比位运算更高。
二、集合与元素
通常会用到移位运算。
其中
注:左移
位相当于乘以 ,右移 位相当于除以 。
| 术语 | 集合 | 位运算 | 集合示例 | 位运算示例 |
|---|---|---|---|---|
| 空集 | ||||
| 单元素集合 | ||||
| 全集 | ||||
| 补集 | ||||
| 属于 | ||||
| 不属于 | ||||
| 添加元素 | ||||
| 删除元素 | ||||
| 删除元素(一定在集合中) | ||||
| 删除最小元素 | 见下 |
s = 101100
s-1 = 101011 // 最低位的 1 变成 0,同时 1 右边的 0 都取反,变成 1
s&(s-1) = 101000特别地,如果
此外,编程语言提供了一些和二进制有关的库函数,例如:
- 计算二进制中的
的个数,也就是集合大小; - 计算二进制长度,减一后得到集合最大元素;
- 计算二进制尾零个数,也就是集合最小元素。
调用这些函数的时间复杂度都是
| 术语 | Python | Java | C++ | Go |
|---|---|---|---|---|
| 集合大小 | s.bit_count() | Integer.bitCount(s) | __builtin_popcount(s) | bits.OnesCount(s) |
| 二进制长度 | s.bit_length() | 32-Integer.numberOfLeadingZeros(s) | __lg(s)+1 | bits.Len(s) |
| 集合最大元素 | s.bit_length()-1 | 31-Integer.numberOfLeadingZeros(s) | __lg(s) | bits.Len(s)-1 |
| 集合最小元素 | (s&-s).bit_length()-1 | Integer.numberOfTrailingZeros(s) | __builtin_ctz(s) | bits.TrailingZeros(s) |
请特别注意 __lg(0) 和 __builtin_ctz(0) 是未定义行为。其他语言请查阅 API 文档。
此外,对于 C++ 的 long long,需使用相应的 __builtin_popcountll 等函数,即函数名后缀添加 ll(两个小写字母 L)。__lg 支持 long long。
特别地,只包含最小元素的子集,即二进制最低 s & -s 算出。举例说明:
s = 101100
~s = 010011
(~s)+1 = 010100 // 根据补码的定义,这就是 -s => s 的最低 1 左侧取反,右侧不变
s & -s = 000100 // lowbit三、遍历集合
设元素范围从
for i in range(n):
if (s >> i) & 1: # i 在 s 中
# 处理 i 的逻辑for (int i = 0; i < n; i++) {
if (((s >> i) & 1) == 1) { // i 在 s 中
// 处理 i 的逻辑
}
}for (int i = 0; i < n; i++) {
if ((s >> i) & 1) { // i 在 s 中
// 处理 i 的逻辑
}
}for i := 0; i < n; i++ {
if s>>i&1 == 1 { // i 在 s 中
// 处理 i 的逻辑
}
}也可以直接遍历集合
t = s
while t:
lowbit = t & -t
t ^= lowbit
i = lowbit.bit_length() - 1
# 处理 i 的逻辑for (int t = s; t > 0; t &= t - 1) {
int i = Integer.numberOfTrailingZeros(t);
// 处理 i 的逻辑
}for (int t = s; t; t &= t - 1) {
int i = __builtin_ctz(t);
// 处理 i 的逻辑
}for t := uint(s); t > 0; t &= t - 1 {
i := bits.TrailingZeros(t)
// 处理 i 的逻辑
}四、枚举集合
§4.1 枚举所有集合
设元素范围从
for s in range(1 << n):
# 处理 s 的逻辑for (int s = 0; s < (1 << n); s++) {
// 处理 s 的逻辑
}for (int s = 0; s < (1 << n); s++) {
// 处理 s 的逻辑
}for s := 0; s < 1<<n; s++ {
// 处理 s 的逻辑
}§4.2 枚举非空子集
设集合为
sub = s
while sub:
# 处理 sub 的逻辑
sub = (sub - 1) & sfor (int sub = s; sub > 0; sub = (sub - 1) & s) {
// 处理 sub 的逻辑
}for (int sub = s; sub; sub = (sub - 1) & s) {
// 处理 sub 的逻辑
}for sub := s; sub > 0; sub = (sub - 1) & s {
// 处理 sub 的逻辑
}为什么要写成 sub = (sub - 1) & s 呢?
暴力做法是从
把所有的合法子集按顺序列出来,会发现我们做的相当于「压缩版」的二进制减法,例如
如果忽略掉
如何快速跳到下一个子集呢?比如,怎么从
- 普通的二进制减法,是
,也就是把最低位的 变成 ,同时把最低位的 右边的 都变成 。 - 压缩版的二进制减法也是类似的,对于
,也会把最低位的 变成 ,对于最低位的 右边的 ,并不是都变成 ,只有在 中的 才会变成 。怎么做到?减一后 就行,也就是 。
§4.3 枚举子集(包含空集)
如果要从大到小枚举
sub = s
while True:
# 处理 sub 的逻辑
if sub == 0:
break
sub = (sub - 1) & sint sub = s;
do {
// 处理 sub 的逻辑
sub = (sub - 1) & s;
} while (sub != s);int sub = s;
do {
// 处理 sub 的逻辑
sub = (sub - 1) & s;
} while (sub != s);for sub := s; ; sub = (sub - 1) & s {
// 处理 sub 的逻辑
if sub == 0 {
break
}
}其中 Java 和 C++ 的原理是,当
注:还可以枚举全集
§4.4 枚举超集
如果
枚举超集的原理和上文枚举子集是类似的,这里通过或运算保证枚举的集合
枚举
s = t
while s < (1 << n):
# 处理 s 的逻辑
s = (s + 1) | tfor (int s = t; s < (1 << n); s = (s + 1) | t) {
// 处理 s 的逻辑
}for (int s = t; s < (1 << n); s = (s + 1) | t) {
// 处理 s 的逻辑
}for s := t; s < 1<<n; s = (s + 1) | t {
// 处理 s 的逻辑
}练习
完成 位运算题单 的第一章。
其他关联题单:
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府