首页 > 代码库 > 人造奇迹——二进制位运算的运用

人造奇迹——二进制位运算的运用

最后更新:2014年4月30日

1、位运算包括:

这个我觉得大家都会我就随便说下:

位与&,如 101 & 110 = 100

位或|,如 100 | 110 = 110

位非~,如 ~101 = 010

位异或^,如 101 ^ 110 = 011

左移<<,如 011 << 1 = 110

右移>>,如 110 >> 1 = 011

其中,负数位运算的时候,用的是补码而不是原码请注意。

左移的时候,高位溢出的将会被舍弃,低位补0。如 11111111 << 3 = 11111000

右移的时候,低位溢出的将会被舍弃,高位补符号位。如 10000001 >> 1 = 11000000

2、gcc内置函数:

gcc中有为处理二进制而诞生的内置函数,如下:

int __builtin_ffs (unsigned int x)
返回从右往左数第一个1。其中x = 0返回0。
int __builtin_clz (unsigned int x)
返回前导0的个数。
int __builtin_ctz (unsigned int x)
返回末尾0的个数。
int __builtin_popcount (unsigned int x)
返回1的个数。
int __builtin_parity (unsigned int x)
返回1的个数的奇偶性。

3、常用技巧:

(1)lowbit = x & -x

x & -x,它把正整数x的1除了它最后一个1,都变成了0,也可以说成是取出x的最后一个1。

如lowbit(01001010) = 00000010

简单地说,对于一个数,比如01001010,它的相反数的补码为10110110,位与的结果为00000010。

原理很简单,某个正整数x的相反数的补码,等于x所有位取反,然后加1。

设y = ~x + 1,比较x和y,x的最后一个1右边都是0,y的同一个位也是1,右边也全是0(x取反加1后,最后一个1变成0,后面的0都变成1,加1后,进位,又变回来了)。而在x的最后一个1的左边和y完全相反。那么x & y就剩下x的最后一个1的位置有1,其余全是0。

这个位运算技巧在树状数组中基本上都要用到。

(2)x & (x-1) == 0 → x是2的倍数

有且只有00010000这种数,减一之后是00001111,与原数每一个位置都不同。

这个位运算的技巧可用于初始化RMQ用的Sparse Table算法。

(3)int fast_max(int x, int y) { return (((x - y) >> 31) & (x ^ y)) ^ x;}

  int fast_min(int x, int y) { return (((y - x) >> 31) & (x ^ y)) ^ y;}

可用于快速得到x和y的最大值,比较的条件运算的速度是比较慢的(注意这个是32位有符号整数才能用,否则要修改)。

首先令z = (x - y) >> 31,若x < y,有x - y < 0,那么x - y的最高位为1,右移31位,得到32个位都是1的z。同理若x ≥ y,那么z = 0。

令 p = x ^ y,那么p的某一位为1当且仅当x和y的那一位不x同为1或不同为0,通俗地说,p就是x和y的“差异”。那么可以得到x ^ p = y,y ^ p = x。

那么,若x < y,有z & p = p,返回值便是x ^ p = y。否则,z & p = 0,返回值为x ^ 0 = x。

4、状态压缩

所谓状态压缩,即把一个集合{0,1,……,n-1},其中选用1表示,不选用0表示,合起来就可以用一个int的二进制表示。那么比如集合{‘a‘, ‘b‘, ‘c‘, ‘d‘},其子集{‘b‘, ‘d‘}就可以用二进制数0101即十进制数5表示。状态压缩DP必备。

(1)测试第 i 个元素是否被选上:

bool check(int state, int i) {

  return (state >> i) & 1;

}

(2)把第 i 个元素设为选择(即1):

int set1(int state, int i) {

  return state | (1 << i);

}

(3)把第 i 个元素设置为不选择(即0):

int set0(int state, int i) {

  return state & ~(1 << i);

}

(4)检查是否有两位选择的元素相邻:

bool check(int state) {

  return (state & (state >> 1)) == 0;

}

5、枚举状态

  用二进制来枚举状态,要比写一个dfs来枚举要简单快速地多。

(1)枚举{0,1,……,n-1}的所有子集:

for(int s = 0; s < 1 << n; ++s) {/*对子集s进行处理*/}

(2)枚举某个集合sup(如01101101)的子集:

像上面那样枚举判断的话,会用很多重复状态,太浪费了。

从大到小枚举,令sub = sup。每次对sub减1,然后位与sup,就能得到比原sub恰好小1的sup的子集。

int sub = sup;

do {

  //对子集sub进行处理

  sub = (sub - 1) & sup;

} while(sub != sup);//处理完sup = 0后,会有sup = -1

(3)枚举集合{0,1,……,n-1}的所包含的大小为k的子集:

首先得到字典序最小的子集comb = (1<<k)-1。

每次循环,取出最低位x = comb & -comb,令y = comb + x。此时x为comb的最低位的1,y把comb最低位的1开始,往左连续的1变成的0,这些1左边的第一个0变成了1。

那么~y和comb位与就得到了comb从最后一位1开始,往左的所有连续的1组成的数。

令z = ~y & comb,将z的1右移到最低位,这个用z/x可以得到,然后再右移一位,删掉最后的一个1。

最后再位与y,就把comb的最后面的连续的1的前面的0变成了1,其余的1删掉一个以后,移到最右边,就可得到comb的下一个集合。

int comb = (1 << k) - 1;

while(comb < 1 << n) {

  //对子集comb进行处理

  int x = comb & -comb, y = comb + x;

  comb = ((comb & ~y) / x >> 1) | y;

}