[toc]
位运算就是基于整数的二进制表示进行的运算。由于计算机内部就是以二进制来存储数据,位运算是相当快的。
常用的运算符共 6 种,分别为与( &
)、或( |
)、异或( ^
)、取反( ~
)、左移( <<
)和右移( >>
)。
与、或、异或
与( &
)或( |
)和异或( ^
)这三者都是两数间的运算,因此在这里一起讲解。
它们都是将两个整数作为二进制数,对二进制表示中的每一位逐一运算。
运算符 | 解释 |
---|---|
& |
只有两个对应位都为 1 时才为 1 |
` | ` |
^ |
只有两个对应位不同时才为 1 |
异或运算的逆运算是它本身,也就是说两次异或同一个数最后结果不变,即 a ^ b ^ b = a 。
取反
取反是对一个数 $num$ 进行的计算,即单目运算。
~
把 $num$ 的补码中的 0 和 1 全部取反(0 变为 1,1 变为 0)。有符号整数的符号位在 ~
运算中同样会取反。
补码:在二进制表示下,正数和 0 的补码为其本身,负数的补码是将其对应正数按位取反后加一。
举例(有符号整数):
$$
\begin{aligned}
5&=(00000101)_2\
\text{}5&=(11111010)_2=-6\}(-5)&=(00000100)_2=4
-5\text{ 的补码}&=(11111011)_2\
\text{
\end{aligned}
$$
左移和右移
num << i
表示将 $num$ 的二进制表示向左移动 $i$ 位所得的值。
num >> i
表示将 $num$ 的二进制表示向右移动 $i$ 位所得的值。
举例:
$$
\begin{aligned}
11&=(00001011)_2\
11<<3&=(01011000)_2=88\
11>>2&=(00000010)_2=2
\end{aligned}
$$
移位运算中如果出现如下情况,则其行为未定义:
- 右操作数(即移位数)为负值;
- 右操作数大于等于左操作数的位数;
复合赋值位运算符
和 +=
, -=
等运算符类似,位运算也有复合赋值运算符: &=
, |=
, ^=
, <<=
, >>=
。(取反是单目运算,所以没有。)
关于优先级
位运算的优先级低于算术运算符(除了取反),而按位与、按位或及异或低于比较运算符(详见 运算页面 ),所以使用时需多加注意,在必要时添加括号。
位运算的应用
位运算一般有三种作用:
高效地进行某些运算,代替其它低效的方式。
表示集合。(常用于 状压 DP 。)
题目本来就要求进行位运算。
需要注意的是,用位运算代替其它运算方式(即第一种应用)在很多时候并不能带来太大的优化,反而会使代码变得复杂,使用时需要斟酌。(但像“乘 2 的非负整数次幂”和“除以 2 的非负整数次幂”就最好使用位运算,因为此时使用位运算可以优化复杂度。)
乘 2 的非负整数次幂
1 | int mulPowerOfTwo(int n, int m) { // 计算 n*(2^m) |
除以 2 的非负整数次幂
1 | int divPowerOfTwo(int n, int m) { // 计算 n/(2^m) |
!!! warning
我们平常写的除法是向 0 取整,而这里的右移是向下取整(注意这里的区别),即当数大于等于 0 时两种方法等价,当数小于 0 时会有区别,如: -1 / 2
的值为 $0$ ,而 -1 >> 1
的值为 $-1$ 。
判断一个数是不是 2 的非负整数次幂
1 | bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; } |
对 2 的非负整数次幂取模
1 | int modPowerOfTwo(int x, int mod) { return x & (mod - 1); } |
取绝对值
在某些机器上,效率比 n > 0 ? n : -n
高。
1 | int Abs(int n) { |
取两个数的最大/最小值
在某些机器上,效率比 a > b ? a : b
高。
1 | // 如果 a>=b,(a-b)>>31 为 0,否则为 -1 |
判断符号是否相同
1 | bool isSameSign(int x, int y) { // 有 0 的情况例外 |
交换两个数
note: “该方法具有局限性”
这种方式只能用来交换两个整数,使用范围有限。
对于一般情况下的交换操作,推荐直接调用 `algorithm` 库中的 `std::swap` 函数。
1 | void swap(int &a, int &b) { a ^= b ^= a ^= b; } |
获取一个数二进制的某一位
1 | // 获取 a 的第 b 位,最低位编号为 0 |
将一个数二进制的某一位设置为 0
1 | // 将 a 的第 b 位设置为 0 ,最低位编号为 0 |
将一个数二进制的某一位设置为 1
1 | // 将 a 的第 b 位设置为 1 ,最低位编号为 0 |
将一个数二进制的某一位取反
1 | // 将 a 的第 b 位取反 ,最低位编号为 0 |
表示集合
一个数的二进制表示可以看作是一个集合(0 表示不在集合中,1 表示在集合中)。比如集合 {1, 3, 4, 8}
,可以表示成 $(100011010)_2$ 。而对应的位运算也就可以看作是对集合进行的操作。
操作 | 集合表示 | 位运算语句 |
---|---|---|
交集 | $a \cap b$ | a & b |
并集 | $a \cup b$ | `a |
补集 | $\bar{a}$ | ~a (全集为二进制都是 1) |
差集 | $a \setminus b$ | a & (~b) |
对称差 | $a\triangle b$ | a ^ b |
遍历某个集合的子集
1 | // 遍历 u 的非空子集 |
用这种方法可以在 $O(2^{popcount(u)})$ ( $popcount(u)$ 表示 $u$ 二进制中 1 的个数)的时间复杂度内遍历 $u$ 的子集,进而可以在 $O(3^n)$ 的时间复杂度内遍历大小为 $n$ 的集合的每个子集的子集。(复杂度为 $O(3^n)$ 是因为每个元素都有 不在大子集中/只在大子集中/同时在大小子集中 三种状态。)
bitset
如果需要操作的集合非常大,可以使用 bitset 。