从集合论到位运算,常见位运算技巧分类总结!
以集合论为指导,利用位运算实现复杂操作!
本篇博客受到 灵茶山艾府 的启发,并结合了自己在实践过程中的经验。经实践,在集合论的指导下使用位运算能够大大地提高对某些问题的求解效率和质量。 总的来说,使用位运算可以极大减少内存占用,而同时将时间复杂度降低至与使用复杂的数据结构相当。
从集合到位运算
在高中,我们学了 集合论 (set theory) 的相关知识。例如,包含若干整数的集合 HashSet
。在集合论中,有交集 O(N)
。那么,有没有效率更高的做法呢?有的,兄弟。有的。
集合可以用二进制表示,二进制从低到高第
集合与集合
其中
术语 | 集合运算 | 位运算 | 集合示例 | 位运算示例 |
---|---|---|---|---|
交集 | ||||
并集 | ||||
对称差 | ||||
差 | ||||
子集差 | ||||
包含于 |
集合与元素
通常会用到位移运算。其中 << 表示左移,>> 表示右移。左移
术语 | 集合运算 | 位运算 | 集合示例 | 位运算示例 |
---|---|---|---|---|
空集 | ||||
单元素集合 | ||||
全集 | ||||
补集 | ||||
属于 | ||||
不属于 | ||||
添加元素 | ||||
删除元素 | ||||
删除在集合中元素 | ||||
删除最小元素 |
此外,编程语言提供了一些和二进制有关的库函数,例如:
- 计算二进制中的 1 的个数,也就是集合大小;
- 计算二进制长度,减一后得到集合最大元素;
- 计算二进制尾零个数,也就是集合最小元素。
调用这些函数的时间复杂度都是 O(1)。
术语 | 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) |
请特别注意 s=0
的情况。对于 C++ 来说,__lg(0)
和 __builtin_ctz(0)
是未定义行为。其他语言请查阅 API 文档。此外,对于 C++ 的 long long
,需使用相应的 __builtin_popcountll
等函数,即函数名后缀添加 ll
(两个小写字母 L)。__lg
支持 long long
。
除上述所举操作外,还有获取集合的只包含最小元素的子集的操作。 即二进制最低 1 及其后面的 0,也叫 lsb (最低有效位),可以用 s&-s
算出,它用到了二进制中补码的定义。补码就是按位取反后加 1,举例说明:
遍历集合
设元素范围从
1 |
|
也可以直接遍历集合
1 |
|
枚举集合
枚举所有集合
设元素范围从
1 |
|
枚举非空子集
设集合为
1 |
|
枚举含空子集
从大到小枚举
1 |
|
Gophers’ Hack
枚举全集
1 |
|
枚举超集
如果
1 |
|
来看如下例题
世界杯赢家
题目描述
假设有 n 场比赛,每场比赛均分出胜负,没有平局。请找出没有输掉任何比赛,且至少参加了一场比赛的全部国家,并按国家编号递增输出;若不存在这样的国家,输出空列表。
关于输入
正整数 n,表示比赛场数。包含 n 个元组的列表 arr。其中每个元组由两个元素组成,第一个是获胜的国家编号,第二个是失败的国家编号。比赛场数和国家编号均介于 [1, 100000]。
关于输出
列表,包含没有输掉任何比赛的全部国家编号,但不包括那些没有参与过任何一场比赛的国家。列表的最大长度显然为 1000000。
常规解法
1 |
|
使用位运算的解法
1 |
|
算法 | 时间复杂度 | 空间复杂度 | 关键点 |
---|---|---|---|
常规解法 | 集合与排序主导时间,空间取决于国家数量 | ||
位运算解法 | 位运算和遍历主导时间,空间取决于编号大小 |
- 常规解法遍历所有比赛后,求差集并排序,其时间复杂度为
。 为arr
长度; 为winners
集合大小、可忽略不记; 为结果集合的大小。常规解法用到了 2 个集合,集合的大小取决于国家的数量 。 - 位运算解法用大整数代替集合,用
|=
操作代替集合的add()
操作,结果无需排序,直接遍历输出,其时间复杂度为 。它使用两个大整数来存储winners
和losers
。假设国家的最大编号为 ,那么它需要 位空间。 - 常规解法适合国家编号范围较大但实际参与国家较少(
)的情况,位运算解法适合国家编号范围较小(如 )且结果数量 较大的场景,避免排序开销。