位运算
位运算就是基于整数的二进制表示进行的运算。由于计算机内部就是以二进制来存储数据,位运算是相当快的。
基本的位运算共
种,分别为按位与、按位或、按位异或、按位取反、左移和右移。
为了方便叙述,下文中省略「按位」。
与、或、异或
运算
|
运算符
|
数学符号表示
|
解释
|
与
|
&
|
、
|
只有两个对应位都为
时才为
|
或
|
|
|
、
|
只要两个对应位中有一个
时就为
|
异或
|
^
|
、
|
只有两个对应位不同时才为
|
对一个负数执行左移操作也未定义。
对于右移操作,右侧多余的位将会被舍弃,而左侧较为复杂:对于无符号数,会在左侧补
;而对于有符号数,则会用最高位的数(其实就是符号位,非负数为
,负数为
)补齐。
复合赋值位运算符
C++ 运算符优先级总表
),所以使用时需多加注意,在必要时添加括号。
位运算的应用
-
-
状压 DP
)。
-
题目本来就要求进行位运算。
需要注意的是,用位运算代替其它运算方式(即第一种应用)在很多时候并不能带来太大的优化,反而会使代码变得复杂,使用时需要斟酌。(但像「乘 2 的非负整数次幂」和「除以 2 的非负整数次幂」就最好使用位运算,因为此时使用位运算可以优化复杂度。)
有关 2 的幂的应用
|
void swap(int &a, int &b) { a ^= b ^= a ^= b; }
|
|
// 求 x 的汉明权重
int popcount(int x) {
int cnt = 0;
while (x) {
cnt += x & 1;
x >>= 1;
return cnt;
|
,直到这个数变为
。
代码如下:
|
// 求 x 的汉明权重
int popcount(int x) {
int cnt = 0;
while (x) {
cnt++;
x -= x & -x;
return cnt;
|
构造汉明权重递增的排列
状压 DP
中,按照 popcount 递增的顺序枚举有时可以避免重复枚举状态。这是构造汉明权重递增的排列的一大作用。
下面我们来具体探究如何在
时间内构造汉明权重递增的排列。
我们知道,一个汉明权重为
的最小的整数为
。只要可以在常数时间构造出一个整数汉明权重相等的后继,我们就可以通过枚举汉明权重,从
开始不断寻找下一个数的方式,在
时间内构造出
的符合要求的排列。
而找出一个数
汉明权重相等的后继有这样的思路,以
为例:
这个过程可以用位运算优化:
|
int t = x + (x & -x);
x = t | ((((t&-t)/(x&-x))>>1)-1);
|
-
第一个步骤中,我们把数
加上它的
lowbit
,在二进制表示下,就相当于把
最右边的连续一段
换成它左边的一个
。如刚才提到的二进制数
,它在加上它的
lowbit
后是
。这其实得到了我们答案的前半部分。
-
我们接下来要把答案后面的
补齐,
的
lowbit
是
最右边连续一段
最左边的
移动后的位置,而
的
lowbit
则是
最右边连续一段
最左边的位置。还是以
为例,
,
,
。
-
接下来的除法操作是这种位运算中最难理解的部分,但也是最关键的部分。我们设
原数
最右边连续一段
最高位的
在第
位上(位数从
开始),最低位的
在第
位,
的
lowbit
等于
1 << (r+1)
,
的
lowbit
等于
1 << l
,
(((t&-t)/(x&-x))>>1)
得到的,就是
(1< ,在二进制表示下就是
后面跟上
个零,零的个数正好等于连续
的个数减去
。举我们刚才的数为例,
。把这个数减去
得到的就是我们要补全的低位,或上原来的数就可以得到答案。
所以枚举
按汉明权重递增的排列的完整代码为: