一、位运算实例

1、用一个表达式,判断一个数X是否是2的N次方(2,4,8,16.....),不可用循环语句。

X:2,4,8,16转化成二进制是10,100,1000,10000。如果减1则变成01,011,0111,01111。两者做按位与运算,结果如果为0,则X是2的N次方。

2、统计一个整数的二进制中1的个数

  1. int count_number_of_one(int number)
  2. {
  3.   int counter = 0;
  4.   
  5.   while (number)
  6.   {
  7.    counter++;
  8.    number &= number - 1 ;
  9.   }
  10.   return counter;
  11. }

二、位运算基础

很多高级的动态规划题目或者一些基础的运算往往需要较高的执行效率和较低的空间需求,或者需要表示一些状态集合,而位运算刚好能满足这一切。

很多的时候,恰当的位运算使用也能使程序变得更加简洁和优美。

1、位运算法则

位运算是各位互不影响的,比如A为1010而B为1100,那么有

  1. A&B=1000
  2. A|B=1110
  3. A^B=0110
  4. ~A=11110101 1的个数是取决于A的类型的,这里认为A的类型是8位整型)

另外两种位运算是位移运算a<<ba>>b。前者表示将a的所有位向左移动b位,后者则表示将a的所有位向右移动b位。对于非负整数(往往这也是我们最关心的),新空出的位将会被0取代。

比如A为1001,而B为3,那么A>>B则为1。

大多数情况下可以简单地认为左移b位就是乘以2b,而右移b位则是除以(整除)2b。当然这是存在例外的——对于负数是不能这么简单认为的:比如在GNU GCC/G++ 编译条件下,若A=-1,你会发现对于任何位移运算A<<B,无论B的取值如何,其结果均为-1。因此请注意,在位移运算下务必确保对非负整数进行运算,以免发生不必要的问题。

对于位移运算最常用的操作就是取一个特定的位——比如1< xx>

2、对于集合的表示

大多数时候,我们可以用一个整数来表示一个包含不超过32(当然如果使用64位整型变量也可以是64个)个元素的集合——对于每一个位,如果元素为1,则表示存在当前位所对应的集合成员,如果是0,则表示这个集合成员是不存在的。

比如A=1011 就可以表示集合{0,1,3},而上面提到的1< xx>

下面我们就能推导出一些直观的集合运算

我们定义 ALL_BITS 为全集即各二进制位均为1的数。

集合的并 A|B

集合的交 A&B

集合的差 A& ~B

补集 ALL_BITS^A

添加特定元素 bit A|=1< bit>

清除特定元素bit A^=1< bit>

取出特定元素bit A&=1< bit>

判断是否存在特定元素bit (A&1< bit="0

  

三、基本技术

这里列举一些常用的位运算技术。

1、交换技术

即不利用第三方进行数的交换,这里给出代码段。

  1. swap(a, b){a^=b;b^=a;a^=b;}

2、提取技术

  

这里我们要做的就是找出变量a最低位的1和最高位的1分别在什么位置。通过这些手段我们就能轻松地将一个集合分解为若干个元素。

  

(1)低位技术

低位技术即Lowbit技术。相信熟悉树状数组(BIT)的朋友应该并不陌生。

我们对于一个非0数x,现在提取出其最低位的1。这里我提三种不同的写法。

  1. Lowbit(x)=x&(x^(x-1))
  2. Lowbit(x)=x&~(x-1)
  3. Lowbit(x)=x&-x

注意:这里我们求出的是x中最后一个1表示的数,而非其位置。

可以发现,这三种低位函数的写法可谓大同小异——均涉及到了x&和x-1(其实 –x 可以认为是和 ~(x-1) 等价的,这里利用了负数的存储原理)。

x-1的性质在于:其将一个数最后一个1变成了0,并把原来这个1之后0的位置均变成了1。低位技术正是利用了这个性质。

举一个简单的应用的例子——N<32 span>

  1. Dfs(dep, mask)
  2. {
  3. if(dep == N) output(P);//输出排列
  4.   K = mask;
  5.   while (K < 0)
  6.   {
  7.    P[dep]= Index(K & -K);[g2]//Index(a)表示a是2的多少次方
  8.    Dfs(dep+1, mask ^ (K & -K));
  9.    K ^= K & -K;
  10.   }
  11. }

上述程序的复杂度为严格的O(N!),而非O(NN)。

这里只是一个说明,并没有特指全排列问题——这种方式在很多地方可以大大提高程序效率。

(2)特殊情况下的简单想法

对于低位技术,一个最简单的想法就是按位扫描依次判断当前位是否为1,这个算法似乎是很慢的——对于一个N位的二进制数,最坏情况需要扫描N次!

但是这只是最坏而非一般——当情况特殊一些——我们要求的不是x的最低位,而是要求出1…2N-1这所有数的低位!

那么在这种情况下,看似缓慢的暴力算法其实是非常不错的——这种算法大约只要均摊2次扫描即可完成检索。

这实际上是最快的方式了。

(3)利用分块思想

我们可以打一张1…28-1的表,用于记录每个数的低位(或者是高位),那么对于32位的整数,就可以将其分解为4个8位整数利用预处理得到的表迅速求出低位或者高位了。

(4)利用编译器的内置函数

  

这是C语言的又一大优势。

这里略微提一下两个CPU位处理指令:BSF(前向位扫描)和BSR(反向位扫描)。这两种指令都是内置且非常高效率的。而令人高兴的是——GNU编译器就存在这两种基于这种原理的位处理函数:__builtin_clz(统计最高位0的个数)和__builtin_ctz(统计低位0的个数)。这是对于C和C++编程者最方便和快捷的位处理方式了。

不过要注意的是——这两个函数对于0都没有定义,因此需要特别小心!

3、计算二进制表示中1的个数

我们很容易判断一个数是不是2的整次幂——比如对于x,那么只要判断x^Lowbit(x)是否为0就可以了。

不过很多时候我们需要统计二进制位中有多少个1(比如当x表示一个集合的时候,我们想知道这个集合中元素的个数),这就要麻烦一点了。

(1)暴力方式

我们可以不断地使用低位技术消去最后一个1。不过这个方法很慢。

(2)预处理

我们可以利用递推形式计算出我们所需要的答案,方式非常简单。

用Cnt[x] 来记录x的二进制表示中1的个数,那么:

Cnt[x] = Cnt[x << 1] + (x & 1)

这样就能在线性时间下计算出结果了。

  

(3)利用内置函数

GNU有一个函数 __builtin_popcount就是实现了我们需要的功能——不过比较遗憾的是,这个函数的实现并不像__builtin_ctz那样利用硬件指令,相反的,它用了一个类似基于预处理方式的按位扫描的实现方式——不过它仍然很高效。

  

(4)有趣的代码

 

这里再给出一段可以实现上述功能的代码,有兴趣的读者可以自行研究。

  1. pop(xx)
  2. {//x为32位有符号非负整数,或者32位无符号类型)
  3. x=x-((x<<1)&0x55555555);
  4.   x=(x&0x33333333)+((x<<2)&0x33333333);
  5.   x=(x+(x<<4))&0x0f0f0f0f;
  6.   x=x+(x<<8);
  7.   x=x+(x<<16);
  8.   return x&0x0000003f;
  9. }

4、枚举子集

当一个数的二进制表示一个集合的时候,位运算的一个巨大优点在于其可以非常轻松和高效地枚举当前集合的所有子集。它的性质在于——如果A是B的真子集,那么可以保证枚举A子集的次数一定小于枚举B的子集的次数。这一点可以大大提高很多压位动态规划题目的实现效率。

(1)暴力的方式

最暴力的方式莫过于枚举所有可能的集合,然后一一判断是否为当前集合的子集。

如果需要枚举的集合是N个元素的集合,那么对所有可能集合都进行一次枚举操作,花费的时间为O((2N)2)=O(4N)。

(2)高效的方式

假设全集有N个元素,那么所有可能的集合就有2N个,对于任意子集S,用N位2进制简单枚举S的所有子集,个数就有2N个,因此如果对所有集合都进行这样一次枚举操作,那么总的时间复杂度就是O((2N)2)=O(4N)。高效的方式。

这里的技巧和低位技术的技巧是类似的——当我们取出最后一个1的时候,这个1将变成0,而比其低位的0将变成1。

与低位技术不同的是,我们并不是要提出某一位1,而是要去除某一位的1,并补上一些我们需要的1。

所以假设当前集合为SuperSet,那么枚举的代码段则为

  1. Iterating_All_SubSet(SuperSet)
  2. {
  3. i = SuperSet;
  4.   while (i < 0)
  5.   i = (i - 1) & SuperSet;
  6. }

若当前为N位二进制的集合,并且对所有可行集合进行上述操作,可以证明,操作的总次数为O(3N)。

四、有趣的技巧

1、计算绝对值

  1. abs(x)
  2. {
  3. y = x<<31;
  4.   return(x^y)-y;//也可写作 (x+y)^y
  5. }

这里需要注意的是,上面的x, y 默认为32位有符号整数。

2、按位翻转

  1.   x=((x&0xaaaaaaaa)<<1)|((x&0x55555555)< span>
  2.   x=((x&0xcccccccc)<<2)|((x&0x33333333)< span>
  3.   x=((x&0xf0f0f0f0)<<4)|((x&0x0f0f0f0f)< span>
  4.   x=((x&0xff00ff00)<<8)|((x&0x00ff00ff)< span>
  5.   x=((x&0xffff0000)<<16)|((x&0x0000ffff)< span>

如果无符号32位整数x=311=(100110111)2,那么经过上述操作后x=3967811584=(11101100100000000000000000000000)2。

3、枚举恰好含有k个元素的集合

我们假设全集为含有N个元素为 {0,1,2,…,N-1},那么代码段可以写成:

  1. int s = (1 < k - span>
  2. while (!(s & 1 < N span>
  3. // 由当前集合 s 计算下一个合法的集合
  4. int lo = s & -s; // 求出低位的1
  5. int lz = (s + lo) & ~s; // 求出比lo高的0中,最低位的0
  6. s |= lz; // 将lz代表的元素加入集合s
  7. s &= ~(lz - 1); // 将比lz位置低的元素全部清空
  8. s |= (lz / lo / 2) - 1; // 将集合元素个数补足为k个
  9. }
  10. ```  
  11. 当然最后一句话也可以写成s |= (lz << __builtin_ctz(lo < span>

C语言中的位运算的技巧的更多相关文章

  1. C语言中的位运算和逻辑运算

    这篇文章来自:http://blog.csdn.net/qp120291570/article/details/8708286 位运算 C语言中的位运算包括与(&),或(|),亦或(^),非( ...

  2. C/C++语言中的位运算

    在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作. 一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率.C语 ...

  3. 深入理解计算机系统(2.2)---布尔代数以及C语言上的位运算

    布尔代数上的位运算 布尔代数是一个数学知识体系,它在0和1的二进制值上演化而来的. 我们不需要去彻底的了解这个知识体系,但是里面定义了几种二进制的运算,却是我们在平时的编程过程当中也会遇到的.这四种运 ...

  4. PHP中的位运算与位移运算(其它语言通用)

    /* PHP中的位运算与位移运算 ======================= 二进制Binary:0,1 逢二进1,易于电子信号的传输 原码.反码.补码 二进制最高位是符号位:0为正数,1为负数( ...

  5. C语言中两位ASCII码可以表示汉字

    最近偶然有人问到这个相关字符编码的问题,所以百度了下参考了这两个资料,进行了简单分析. ******************************************************** ...

  6. Google Earth Engine 中的位运算

    Google Earth Engine中的位运算 按位运算是编程中一个难点,同时也是在我们后续处理影像数据,尤其要使用影像自带的波段比如QA波段经常会用到的一个东西.通过按位运算我们可以筛选出我们想要 ...

  7. 关于C/C++中的位运算技巧

    本篇文章讲述在学习CSAPP位运算LAB时的一些心得. 移位运算的小技巧 C/C++对于移位运算具有不同的策略,对于无符号数,左右移位为逻辑移位,也就是直接移位:对于有符号数,采用算术移位的方式,即左 ...

  8. ACM中的位运算技巧

    听说位运算挺好玩的,那这节总结一下ACM中可能用到的位运算技巧. XOR运算极为重要!!(过[LC136](只出现一次的数字 - 力扣(LeetCode)):数组中每个数字都出现两次,只有一个出现一次 ...

  9. C++中的位运算总结

    1)位运算 位运算是指对转换成二进制的数字进行每一位上的0.1的运算,运算涉及到五种运算:与(&),或(|),异或(^),左移(<<),右移(>>). 如下表所示:   ...

随机推荐

  1. springmvc3.1.1+hibernate4

    上篇介绍了基本的配置,这篇着重介绍与hibernate4整合. 1.web.xml文件中加入spring-hibernate的配置.新的web.xml文件内容如下: <?xml version= ...

  2. c#后台输出javascript语句和一些通用验证的类

    大家在用MVC的时候,经常会用到MODEL层的验证或者是正则表达式,我这边看到了一篇不错的文章,转载过来http://blog.csdn.net/accpxcb/article/details/311 ...

  3. Html 导航

    首页,来一个比较简单的例子热热身,相信有点css基础的人都可以看懂的.自所以,写一些教程,或许这样的教程已经泛滥啦,但是,还是想理理自己的思想来帮助自己及引导初学者更好的理解css. 1.竖直排列导航 ...

  4. c# 窗体最小化后截图实现

    我们知道,当我们需要对Control进行截图时,我们可以使用Control.DrawToBitmap()进行截图, 那么问题来了,当我们的窗体最小化了,我们还怎么截图呢? 当窗体最小化的时候,我们知道 ...

  5. net破解一(反编译,反混淆-剥壳,工具推荐)

    net破解一(反编译,反混淆-剥壳,工具推荐) 大家好,前段时间做数据分析,需要解析对方数据,而数据文件是对方公司内部的生成方式,完全不知道它是怎么生成的. 不过还好能拿到客户端(正好是C#开发)所以 ...

  6. 代码高亮插件推荐——SyntaxHighlighter++

    SyntaxHighlighter++这个插件的最大的优点就是可以在编辑器的下方有一个输入框,里面可以输入代码,然后插入到文章中.就不用编辑文章的时候,在可视化和文本之间来回切换了.非常适合不熟悉ht ...

  7. CSS盒子的浮动

    web前端学习笔记(CSS盒子的浮动) 在标准流中,一个块级元素在水平方向会自动伸展,直到包含它的元素的边界:而在竖直方向和兄弟元素依次排列,不能并排.使用“浮动”方式后,块级元素的表现就会有所不同. ...

  8. .net mvc页面UI之Jquery博客日历控件

    摘要:最近在做一个博客系统,其他需要用到博客日历控件,网上搜索了很多资料,其中大部分都是javascript的,经过总结使用jquery实现了博客日历效果.代码如下: 原文链接转载请注明:http:/ ...

  9. MEF只导出类的成员

    MEF只导出类的成员 通过前面两篇文章的介绍,相信各位会明白MEF中有不少实用价值.上一文中我们也讨论了导入与导出,对于导出导入,今天我们再深入一点点,嗯,只是深入一点点而已,不会很难的,请大家务必放 ...

  10. Web Api初试

    Web Api初试 前言 ASP.NET Web API 与之前的内建HTTP服务解决方案的不同之处在于,它一开始就是围绕HTTP协议及其消息语义构建起来的.与WCF REST或ASP.NET AJA ...