位运算¶
位运算优先级:位运算的优先级低于算术运算符(除了取反),而按位与、按位或及异或低于比较运算符
典型应用:
位取反运算符¶
~
的作用是对数字n
的二进制表示进行按位取反,如果n
是非负整数(也就是自然数),则~n = -1 * (n + 1)
,也就是~0=-1, ~5=-6
。如果n
是负数,则~n = -1 * n - 1
,也就是~5 = 4
。
正数的补码是其原码,负数的补码是其绝对值 按位取反后加一,符号位(即第一位)不变。
非负数的表示是其原码,负数用其相反数的补码表示。所以读到一个二进制数其最高位为1,则先去掉最高位,剩余的二进制表示减1然后按位取反得到数字,然后加上符号。 $$ \sim n = -(n+1), \quad n \geq 0\ \sim n = -n - 1, \quad n < 0\ \therefore \sim n = -n-1\ $$
计算乘2的非负整数次幂¶
假设数字n表示成二进制有k-1位(从0开始计数),则数字可以被表示成: $$ n = b_{k-1}2{k-1}+b_{k-2}2{k-2}+\cdots +b_0\ b_i = 0 或1,其中0 \leq i \leq k-1 \ n \times (2^m) = (b_{k-1}2{k-1}+b_{k-2}2{k-2}+\cdots +b_0) \times 2^m \ = (b_{k-1 }2{k-1+m}+b_{k-2}2{k-2+m}+\cdots +b_02^m) $$ 从表达式可以看出,相当于数字n右移m位,所以上式相等。
除以2的非负整数次幂¶
证明方法同上。
判断一个数是不是2的正整数次幂¶
- LeetCode 231.2的幂
bool isPowerOfTwo(int n)
{
return n && !(n & (n - 1));
}
证明:2的正整数次幂首先这个数n必须是正整数,如果是2的正整数次幂,那么其二进制表示就是10000\cdots 000,n-1的二进制表示就会成为0111\cdots 1111,所以进行与运算结果为零。
对2的非负整数次幂取模¶
这里mod
是2的非负整数次幂值,如1,2,4,8,16,x
是一个非负整数。求x % mod
的值。特殊的,当mod = 2
时退化成x & 1
,和判断奇偶数的形式一样了。
int modPowerOfTwo(int x, int mod)
{
return x & (mod - 1);
}
证明:设mod = 2^m,将数字x表示成二进制:
$$
x = b_{k-1}2{k-1}+b_{k-2}2{k-2}+\cdots +b_0\
x / mod = x >> m \
= (b_{k-1}2{k-1}+b_{k-2}2{k-2}+\cdots +b_0) / 2^m \
= (b_{k-1}2{k-1}+b_{k-2}2{k-2}+\cdots +b_m2^m + b_{m-1}2^{m-1} + \cdots+b_0) / 2^m \
=\frac{b_{k-1}2{k-1}+b_{k-2}2{k-2}+\cdots +b_m2m}{2m} + \frac{b_{m-1}2^{m-1} + \cdots+b_0}{2^m} \
$$
前半部分就是商,也就是右移m位后留下来的部分,那么左半部分因为指数小于2^m,所以后面的位组成的十进制数肯定小于mod
也就是2^m。意味着b_{m-1}2^{m-1} +\cdots+b_0就是余数,也就是要取出原数字x的后m位,mod
转成二进制就是10000\cdots000,那么只需要减1再进行与运算即可。
计算绝对值¶
#include <climits> //CHAR_BIT在此头文件定义,包括INT_MAX和INT_MIN等常量
int ABS(int n)
{
const int mask = n >> (sizeof(int) * CHAR_BIT - 1);
return (n ^ mask) - mask;
}
这里第一步计算mask
是为了取出数字n
的最高位来判断符号。如果n
是自然数,则mask
为0,则n^0 = n
,所以(n ^ mask) - mask = n
。括号是不能少的,因为减法运算(算术运算)的优先级要高于位运算。
如果n
是负数,则mask=-1
,则n ^ mask
需要计算n
和-1
的补码然后进行按位异或。-1
的补码各个二进制位全是1,n
的补码与之异或相当于全部二进制位按位取反,得到的是-n - 1
,所以结果是-n - 1 - (-1) = -n
得到的就是绝对值。
计算两数中的较大值和较小值¶
#include <climits>
int r;
r = y ^ ((x ^ y) & -(x < y)); //min(x, y)
r = x ^ ((x ^ y) & -(x < y)); //max(x, y)
// If we know INT_MIN <= x - y <= INT_MAX, the following is much faster
r = y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); //min(x, y)
r = x + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); //max(x, y)
假如x < y
,那么-(x < y) = -1
,刚才已经分析了-1
的补码各个二进制位都是1,所以(x^y) & (-1) = x ^y
,那么r = y ^ (x ^ y) = x = min(x, y)
,对于max(x, y)
的分析同理。
如果x > y
,那么-(x < y) = 0
,则(x^y) & 0= 0
, 那么y ^ 0 = y
即为min(x, y)
。
建议采用第一种方案,第二种虽然速度快但是多了限制条件,很容易在不察觉的情况下引发bug
。
判断两个数符号是否相同¶
#include <climits>
bool isSameSign(int a, int b)
{
if (!a || !b) return false;//0的特殊情况要单独判断
return (x ^ y) >= 0;
}
另一种方法:
#include <climits>
bool isSameSign(int a, int b)
{
return !((a >> 31) ^ (b >> 31)); //判断最高位即可
}
交换两个数¶
void Swap(int & a, int & b)
{
return a ^= b ^= a ^= b;
}
上面的代码拆解开来: $$ a = a \land b \ b = b \land a = b \land a \land b = a \ a = a \land b = a \land b \land b \land a \land b = b \ $$ 于是在不使用中间变量的情况下完成两数的交换。
数字越界判断¶
比如判断0 <= a && a <= INT_MAX
需要进行两次判断,可以用位运算修改为无分支的情况:
#include <climits>
统计数字n的二进制位中1的个数(汉明重量)¶
统计二进制1的个数可以分别获取每个二进制位数,然后再统计其1的个数,此方法效率比较低,用位运算可以加快这一操作。以 34520 为例,我们计算其 a &= (a-1)
的结果:
- 第一次:计算前:1000 0110 1101 1000 计算后:1000 0110 1101 0000
- 第二次:计算前:1000 0110 1101 0000 计算后:1000 0110 1100 0000
- 第二次:计算前:1000 0110 1100 0000 计算后:1000 0110 1000 0000 我们发现,没计算一次二进制中就少了一个 1,则我们可以通过下面方法去统计:
int cnt = 0;
while(a){
a = a & (a - 1);
++cnt;
}
计算带符号整数的最大值和最小值¶
也就是计算包含在头文件<climits>
里的INT_MIN
和INT_MAX
,其中一种写法是直接定义:
const int INF = 0x0ffffff;
经过测试,这种写法其实并不能取到正整数的最大值,比如:
#include <iostream>
#include <climits>
using namespace std;
int main()
{
const int INF = 0x0ffffff;
cout << INF << endl;
cout << INT_MAX << endl;
return 0;
}
# result
16777215
2147483647
发现两者并不相同,INF < INT_MAX
。另外的方法就是自己写一个函数来计算出INT_MIN
和INT_MAX
:
#include <iostream>
#include <climits>
using namespace std;
int getMax()
{
int mask = (sizeof(int) * CHAR_BIT) - 1;
return ~(1 << mask); //也可以写为(1 << mask) - 1
}
int getMin()
{
int mask = (sizeof(int) * CHAR_BIT) - 1;
return 1 << mask;
}
int main()
{
cout << INT_MAX << endl;
cout << getMax() << endl;
cout << getMin() << endl;
cout << INT_MIN << endl;
return 0;
}
# result
2147483647
2147483647
-2147483648
-2147483648
原理是先计算出对于一台计算机是用多少位来表示int
的,因为不同位数的计算机可能有所不同,所以保险起见还是用int mask = (sizeof(int) * CHAR_BIT) - 1;
,然后将数字1移动到最高位,那么最高位是1,后面的所有位都是0,然后按位取反,则最高位是0,剩余的位全是1,显然就是INT_MAX
。
计算INT_MIN
是直接1 << mask
,也就是最高位是1,剩余位全是0.最高位为1代表着这个数是负数,那么它实际表示的数是除了最高位以外其余位按位取反得到的是INT_MAX
,然后这个数减1,加上负号,所以表达的是INT_MIN
。
计算两个数的中间值¶
这个在二分法里面经常用到,两个数字a, b
,如果直接计算(a+b)/2
可能存在数据溢出,那么保险的方法是
int mid = a + ((b - a) >> 1); //也就是a + (b - a) / 2
如果转化成用位运算表示:
int mid = (a & b) + ((a ^ b) >> 1);
原理分析的参考:https://blog.csdn.net/QQ1910084514/article/details/80092380
考虑两个数的二进制表示,相加也就是对应的采用指数表示的二进制位相加,即: $$ a = a_{k-1} 2^{k-1} + \cdots + a_0 \ b= b_{k-1} 2^{k-1} + \cdots + b_0 \ 对应位相加:(a_i + b_i)2^i $$ 那么如果对应位同为1,则表示成2\times2^i,一个为0一个为1则是1\times2^i,所以a+b可以表示成 $$ a +b = (2 \times (a \& b)) + ( a\land b)\ \therefore (a+b) / 2 = (a \& b) + ((a \land b) >> 1)\ $$
集合的整数表示¶
来源于《挑战程序设计竞赛》“反转”章节。将集合用整数表示,便于操作还能加快运算速度。
对于整数集合S = {0,1,2,...,n-1}
,可以用一个整数来表示,其实本质就是每个数字对应于一个二进制位,所以有f(S) = \sum_{i\in S} 2^i
于是有以下一些常用技巧:
- 空集
0
- 只含有第
i
个(从下标0开始计数)元素
1 << i
- 含有集合S的全部元素的表示,相当于求\sum_{i = 0}^{n - 1}2^i = 2^n - 1。注意位运算优先级低于算术运算,所以要加括号。
(1 << n) - 1
- 判断第
i
个(下标从0开始计数)元素是否属于集合,相当于检验i
对应的二进制位是否为1
if (S >> i & 1)
- 向集合中加入第
i
个元素,相当于将i
对应的二进制位变为1
S | (1 << i)
- 从集合中除去第
i
个元素,相当于将i
对应的二进制位变为0
S & ~(1 << i)
- 集合S和集合T的并集
S | T
- 集合S和集合T的交集
S & T
- 将集合
{0,1,2,...,n-1}
全部列举出来,将会按照0,1,01,……,{0,1,2,...,n-1}
的顺序枚举出来
for (int s = 0; s < (1 << n); ++s) {
//对集合S的操作
}
- 特殊集合的子集的枚举,比如由0,2,4等组成的集合表示成二进制是
0....10101
,也就是对应的二进制位为1,那么类似这样的集合的子集去枚举
int sub = s;
do {
/*
{对子集的处理操作}
*/
sub = (sub - 1) & s; //寻找下一个子集
} while (sub != s); //所有子集枚举完成后sub = -1,-1 & s = s,退出循环
比如只含0,2,4的集合,按照上面的方法,会按照下面的顺序进行枚举:
4 2 0
4 0
4
2 0
2
0
- 枚举集合
{0,1,2,...,n-1}
的所有长度为k
的集合。
int s = (1 << k) - 1;
while (s < (1 << n)) {
/*
{针对长度为k的集合的处理}
*/
int x = s & -s; //取出最后一个1
int t = s + x; //将最后连续的1最左边的1往左移动1位
s = (((s ^ t) / x) >> 2) | t;
}
参考了《挑战程序设计竞赛》和这篇博客
与这道题相关联的变形可以是POJ 2453,给定一个**正整数**N,求最小的、比N大的**正整数**M,使得M与N的二进制表示中有相同数目的1。基本上是同一种方法,两者的本质都是给定一个已知满足条件的数字,然后去寻找“紧邻”的比当前大的下一个数字。
以N=78为例(其二进制表示为1001110),我们的任务是求得最小的比N大,二进制表示中1的个数与N相同的数:83(其二进制表示为1010011)。首先我们要总结出来从78变成83的规律,为了方便,将78和83的二进制写成竖式形式:
78:1 0 0 1 1 1 0
83:1 0 1 0 0 1 1
可以看出,为了得到83,我们只需要对N(78)的二进制中最右边的连续的1位串(加粗标红)进行操作!其过程是:将连续的1位串中最左边的1向左“移动”一位,其他的1位串“移动”到最右边!这即保证了二进制表示中1的个数不变,又保证了新得到的数比原来的数大,并且是最小的。
在上面的描述中我用引号把两个“移动”引起来了,原因是,具体实现时,我们并不是对这些二进制位进行移动,而是通过位操作来达到同样的目的,而这些位操作就是本问题的关键。接下来我将分析前面那个“非常给力的代码”看看它是如何用位操作来实现对这些位的“移动”的。
首先来看语句**int x = N&(-N);**它的功能是找到N(即78)的二进制表示中最右边的1(这个1必定是N的二进制表示中最右边的连续的1位串的开始)。该过程图示如下:
接下来看看**int t = N+x;**该语句实现了“**将连续的1位串中最左边的1向左“移动”一位”**的功能,当然它带来了副作用:使得连续的1位串中其他的1丢失了!其过程如下:
最后的任务就是要将上面丢失的1补上,并放到最右边,这就是语句int ans = t | ((N^ t)/x)>>2;的功能。首先,要知道需要补多少个1,通过分析可以知道需要补上的1的个数等于N的二进制表示中最右边的连续的1位串中1的个数减1,然而如果通过位操作来求得呢?这就是N^ t的功能了,如下图所示,N^t的二进制表示只包含1个连续的1串,并且1的个数正好等于N的二进制表示中最右边的连续的1位串中1的个数加1:
由上面的分析可知,N^ t中的1的个数实际上比我们需要补的1的个数多2!这就使得我们可以通过N^t求得需要补的1的个数,接下来的任务就是如何补上这些1了。
进步一分析得知,N^ t的二进制表示中最低位的1正好与x中那个1对应,因此我们就可以通过(N^ t)/x将这些1全部移到最右边了,然而此时1的个数比我们要补的个数多了2,没关系我们在把结果右移2位就可以了,也就是((N^t)/x)>>2。如此一来我们求得了要补的1的个数和其位置。本段的描述可以用下图形象地表示:
最后我们只需要用**t | ((N^t)/x)>>2;就能得到所求之数了!其过程如下图:**
另外,对于《挑战程序设计竞赛》书中提到的不包含相邻元素的集合的枚举,在这篇博客里给出了扩充的解答。
建议这个部分单独拿出来,仍然属于“位运算”专题
位运算解决n皇后问题¶
Matrix67的第三篇关于二进制的博客。
数字范围按位与¶
- LeetCode 201 数字范围按位与
给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)。
示例 1:
输入: [5,7]
输出: 4
将5,6,7的二进制写出:
5 101
6 110
7 111
思路是去寻找左右边界从右向左第一个不同的位置,最后保留相同的部分即可。因为范围从m
到n
是连续的,那么它们的公共前缀部分设为xxx
,则第一个不相同的位置肯定在n
是1, 在m
里是0,不然违背大小关系。那么从m
增长到n
,则首先需要增长到xxx0111..11
,然后增长到xxx100...00
,然后再增长到n
,那么很显然xxx0111..11
和xxx100...00
按位与,xxx
后面的肯定都是0,那么也就意味着只需要求出m
和n
的相同二进制前缀即可。
class Solution {
public:
int rangeBitwiseAnd(int m, int n) {
std::ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
unsigned int mask = INT_MAX;
while ((m & mask) != (n & mask)) {
mask <<= 1;
}
return mask & m;
}
};
数字的补数¶
- LeetCode 476 数字的补数
- LeetCode 1009 数字的补数(两个题一模一样)
给定一个正整数,输出它的补数。补数是对该数的二进制表示取反。
示例 1:
输入: 5
输出: 2
解释: 5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。
class Solution {
public:
int findComplement(int num) {
std::ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
unsigned int mask = INT_MAX;
while (mask & num) mask <<= 1;
return (~num) & (~mask);
}
};
按位取反很自然的联想到运算符~
,但是如果能够固定数量的按位取反,其实可以考虑利用INT_MAX
的移位操作,不断左移,直到和num
的与运算为0,那么就可以确定需要按位取反的有多少位了,注意输入都是正整数,所以不存在输入为0的情形。另外要用unsigned int
类型。
典型题目¶
出现在《C++程序设计思想与方法》的辅导书里第一章,《C++primer》里的位运算基本知识讲解。
异或运算相关:https://www.cnblogs.com/encodetalker/p/10700434.html
集大成者:https://graphics.stanford.edu/~seander/bithacks.html
leetcode总结:https://blog.csdn.net/EbowTang/article/details/51228929
可以参考:https://www.cnblogs.com/thrillerz/p/4530108.html
https://www.zhihu.com/question/38206659?sort=created
以及Matrix67位运算系列的基础+进阶1+进阶2+实战篇https://blog.csdn.net/wangs915/article/details/8735774
https://blog.csdn.net/leelitian3/article/details/80251902
很不错的总结:
C/C++刁钻问题各个击破之位运算及其应用实例(1) 里面还是存在一些问题,不过其交换数字的第一种方法还是很新奇的。
- LeetCode 136 Single Number
- LeetCode 137 Single Number 2
- LeetCode 260 Single Number 3
- LeetCode 201 数字范围按位与
- LeetCode 476 数字的补数
- POJ 3748 位操作
- POJ 2309
- POJ 3690
- POJ 3220
- POJ 2453(位运算计算二进制1个数相同的下一个数)
- POJ 1222
- POJ 1753 (位运算 + BFS)
- POJ 3279
- HDU 5969
- HDU 5344
- HDU 3257
- HDU 1390
- HDU 5747
- HDU 3935
- HDU 4321
- HDU 3782
- HDU 4825 字典树 + 位运算
- Google Kick Start 2019 Round G(异或运算)