位运算(转载于互联网)

 

 

本文将对基于C语言的位运算进行一些简要总结和归纳,并利用例题进行一些说明。

  一、 C语言说明

  这里将对C语言进行一些基本介绍和分析。

  1.C语言优势

  (1)简洁的代码书写

  C语言的代码往往很短,而且也可以写得很短——C语言拥有丰富的代码技巧,充分掌握能够使得代码变得非常优美短小。

  一个最明显的例子就是C语言中标注程序段的符号为 {},而Pascal语言中为BeginEnd,仅此一项就省去了6个字节的代码量,因此编程时间大大缩短。

  (2)强大的库函数功能

  C++拥有完善的STL类模板,能够实现多种高级数据结构和基本算法操作,并且具有空间小、速度快的特点,而即使是C语言,stdlib库中用非递归实现的快速排序算法也比Pascal语言编写的普通排序代码快了一倍。数学库中的相关函数也比Pascal更全面。

  (3)完善的细节

  首先,C语言中的整数类型当运算越界后将自动 Mod 2p,而在没有打开编译开关的情况下,Pascal语言则会报错。

  另外,Pascal语言的一大缺点就是没有真正意义上的按位取反运算符,这使得Pascal语言的位运算显得残缺。

  2.C语言运算符简单说明

  C语言中的基本位运算符为: &(按位与运算),|(按位或运算),^(按位异或运算),~(按位取反运算),<<(按位左移运算)和>>(按位右移运算)。

  对于&|~,还有它们对应的逻辑运算形式(返回真或假)&&(逻辑运算与),||(逻辑运算或)和!(逻辑运算非)。

  还有一些特殊的运算符如 &=|=^=等为自运算符,比如 A &= B 的含义即为将 A & B 的值赋值给A,也可以写成 A = A&B

  最后,0x开头的数为十六进制数,比如 0x1f 就表示十六进制数(1f16,即31

  二、位运算基础

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

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

  1.位运算法则:

  位运算是各位互不影响的,比如A1010B1100,那么有

  A&B=1000

  A|B=1110

  A^B=0110

  ~A=11110101 1的个数是取决于A的类型的,这里认为A的类型是8位整型)

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

  比如A1001,而B3,那么A<<B则为1001000A>>B则为1

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

  对于位移运算最常用的操作就是取一个特定的位——比如1<<x就可以来表示一个状态集合——集合中仅元素x存在而没有任何其他元素——因为其他的所有位均为0

  2.对于集合的表示

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

  比如A=1011 就可以表示集合{013},而上面提到的1<<x就表示集合{x}

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

  我们定义 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.交换技术

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

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

  2.提取技术

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

  (1)低位技术

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

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

  Lowbit(x)=x&(x^(x-1))

  Lowbit(x)=x&~(x-1)

  Lowbit(x)=x&-x

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

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

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

  举一个简单的应用的例子——N<32的全排列问题。

  Dfs(dep, mask){

      if(dep == N) output(P);//输出排列

    K = mask;

    while (K > 0) {

     P[dep]= Index(K & -K);[g2]//Index(a)表示a2的多少次方

     Dfs(dep+1, mask ^ (K & -K));

     K ^= K & -K;

    }

  }

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

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

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

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

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

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

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

  (3)利用分块思想

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

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

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

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

  不过要注意的是——这两个函数对于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)有趣的代码

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

  pop(xx){//x32位有符号非负整数,或者32位无符号类型)

  x=x-((x>>1)&0x55555555);

  x=(x&0x33333333)+((x>>2)&0x33333333);

  x=(x+(x>>4))&0x0f0f0f0f;

  x=x+(x>>8);

  x=x+(x>>16);

  returnx&0x0000003f;

}

  4.枚举子集

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

  (1)暴力的方式

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

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

  (2)高效的方式

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

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

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

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

  Iterating_All_SubSet(SuperSet) {

  i = SuperSet;

  while (i > 0)

   i = (i - 1) & SuperSet;

  }

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

  四、有趣的技巧

  再提一些在C/C++中更加惊人的位运算技巧——它绝对可以让你在同学面前炫耀一下。

  1.计算绝对值

  abs( x ) {

      y=x>>31 ;

      return(x^y)-y;//也可写作 (x+y)^y

  }

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

  2.按位翻转

x=((x&0xaaaaaaaa)>>1)|((x&0x55555555)<<1);

x=((x&0xcccccccc)>>2)|((x&0x33333333)<<2);

x=((x&0xf0f0f0f0)>>4)|((x&0x0f0f0f0f)<<4);

x=((x&0xff00ff00)>>8)|((x&0x00ff00ff)<<8);

x=((x&0xffff0000)>>16)|((x&0x0000ffff)<<16);

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

  这里不多作解释(其实研读这段代码是很有意思的一件事情),留给有兴趣的读者思考。

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

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

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

  while (!(s & 1 << N)) {

      // 由当前集合 s 计算下一个合法的集合

      int lo = s & -s;       // 求出低位的1

      int lz = (s + lo) & ~s;      // 求出比lo高的0中,最低位的0

      s |= lz;                     // lz代表的元素加入集合s

    s &= ~(lz - 1);              // 将比lz位置低的元素全部清空

    s |= (lz / lo / 2) - 1;      // 将集合元素个数补足为k

}

  当然最后一句话也可以写成s |= (lz >> __builtin_ctz(lo << 1)) 1来避免除法运算。

  五、 实例

  1.格雷码

  【问题描述】

  格雷码是由Frank Gray1940年提出的一种编码方式。N位格雷码是由02N-12N个数组成的数列,并且相邻两个数按2进制位仅一个位不同。

  显然,格雷码是可以循环排列的,比如1位格雷码{0,1}{1,0}均是一种合法的排列。

  不过在通常情况下,都以0为编号为0的格雷码。

  给出格雷码,输出其对应的编号。

  给出编号,输出其对应的格雷码。

  【问题分析】

  这是一个非常经典的问题。先给出前8个格雷码。

 编号 格雷码 二进制

  0    000  000

  1    001  001

  2    011  010

  3    010  011

  4    110  100

  5    111  101

  6    101  110

  7    100  111

  略微分析可以发现可以用这样的方式生成格雷码:

  但是,仔细观察可以发现,格雷码各个数位在翻译过程中是并行的——也就是说各个数位不是相互关联而是独立存在的!

  这一点非常符合位运算的性质!

  因此,我们可以使用位运算非常优美地解决这个问题。

  【问题的解决】

  这里认为所求格雷码和编号均是不超过232-1的。

  2进制转换为格雷码

  unsigned BinaryToGray(unsigned B ) {

      return B ^ (B>>1) ;

  }

  格雷码转换为二进制

  unsigned GrayToBinary(unsigned G) {

      unsigned B ;

      G = G ^ (G>>1) ;

      G = G ^ (G>>2) ;

      G = G ^ (G>>4) ;

      G = G ^ (G>>8) ;

      G = G ^ (G>>16) ;

  [j5]    return G ;

  }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值