天天看点

关于位运算

       关于位运算,自从做过了“黑白棋游戏”之后,深感位运算之强大!在节约时间和空间很有帮助,常被拿来作优化之用。

       学习位运算我个人感觉有两点很重要:1耐心 2精益求精。

在下来的题目分析中,大家将会有强烈的体会。

文章结构:

1.        位运算的基本介绍

a)        移位运算

b)       与运算

c)        或运算

d)       非运算

e)        异或运算

2.        常见注意问题

3.        实战问题解决

a)        位基本操作 PKU 2453

b)       位压缩技术 PKU 2443

c)        位操作在BFS中的优化PKU 2965

4.        参考网页及文献

按位左右移位运算符

按位左右移位运算符是最常用的两个,即便没有用过也用过它的重载:cout<< 和cin>> 吧!

<<1 = * 2

<<2 = * 4

...

>>1 = / 2

>>2 = / 4

...

移位运算符包括:

“>> 右移”;“<< 左移”;“>>> 无符号右移”

例子:

-5>>3=-1

1111 11111111 1111 1111 1111 1111 1011

1111 11111111 1111 1111 1111 1111 1111

其结果与 Math.floor((double)-5/(2*2*2)) 完全相同。

-5<<3=-40

1111 11111111 1111 1111 1111 1111 1011

1111 11111111 1111 1111 1111 1101 1000

其结果与 -5*2*2*2 完全相同。

5>>3=0

0000 00000000 0000 0000 0000 0000 0101

0000 00000000 0000 0000 0000 0000 0000

其结果与 5/(2*2*2) 完全相同。

5<<3=40

0000 00000000 0000 0000 0000 0000 0101

0000 00000000 0000 0000 0000 0010 1000

其结果与 5*2*2*2 完全相同。

-5>>>3=536870911

1111 1111 1111 1111 1111 1111 1111 1011

0001 1111 1111 1111 1111 1111 1111 1111

无论正数、负数,它们的右移、左移、无符号右移 32 位都是其本身,比如 -5<<32=-5、-5>>32=-5、-5>>>32=-5。

一个有趣的现象是,把 1 左移 31 位再右移 31 位,其结果为 -1。

0000 00000000 0000 0000 0000 0000 0001

1000 00000000 0000 0000 0000 0000 0000

1111 11111111 1111 1111 1111 1111 1111

按位与运算(&)

&在C语言编程中怎么用,比如X=2,Y=3,X&Y为多少?

按位与运算 按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1 ,否则为0。参与运算的数以补码方式出现。

例如:2&3可写算式如下: 10 (2的二进制)&11(5的二进制补码) 10 (2的二进制)可见2&3=2。

10

& 11

------

10

在实际编程的应用中,我在判奇偶性的时候喜欢用(n&1),如果为1则是奇数,如果为0则是偶数。因为不管这个数有多数位,决定奇偶的只可能是最后一位。

清零特定位 (mask中特定位置0,其它位为1,s=s&mask)

取某数中指定位 (mask中特定位置1,其它位为0,s=s&mask)

常用来将源操作数某些位置1,其它位不变。 (mask中特定位置1,其它位为0s=s|mask)

按位或运算(|)

按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。参与运算的两个数均以补码出现。

例如:9|5可写算式如下:

    00001001

   | 00000101

    00001101        (十进制为13)可见9|5=13

按位非运算(~)

对一个表达式执行按位“非”(取反)。

result =~ expression

执行按位非操作,该操作的结果如下所示:

0101   (expression)

----

1010   (result)

异或XOR运算(^)

异或的运算方法是一个二进制运算:

 1^1=0

 0^0=0

 1^0=1

 0^1=1

 两者相等为0,不等为1.

 这样我们发现交换两个整数的值时可以不用第三个参数。

 如a=11,b=9.以下是二进制

 a=a^b=1011^1001=0010;

 b=b^a=1001^0010=1011;

 a=a^b=0010^1011=1001;

 这样一来a=9,b=13了。

应用:

使特定位的值取反 (mask中特定位置1,其它位为0 s=s^mask)

不引入第三变量,交换两个变量的值 (设 a=a1,b=b1)

  目 标 操 作 操作后状态

  a=a1^b1 a=a^ba=a1^b1,b=b1

  b=a1^b1^b1 b=a^b a=a1^b1,b=a1

  a=b1^a1^a1 a=a^b a=b1,b=a1

好的,介绍完位运算的基本操作,现在我们看一下位运算中一些常见要注意的问题

1、优先级

这是个非常严重的问题,在进行位运算的时候优先级太容易被忽略掉了

尤其要注意的:

移位运算符, 单目的取反运算符~的优先级比比较运算符高。但是,&, |, ^的优先级是比比较运算符低的!,这点一定要注意

如6 & 6 > 4的结果是0, 而不是 true; (6 & 6) > 4的结果才是true,所以要注意勤加括号

2、速度

位运算的速度是非常快的,你甚至可以忽略他的耗时,但是毕竟操作肯定是有耗损的。

所以,应该尽量使用

^=, |= ,&=之类的操作,比如说a ^= b, 速度比 a = a ^ b快,因为前者是直接在a上进行操作,而 a = a ^ b的第二个a,是一个a的副本,可见在操作中程序对a复制了一次,运算的结果又对原来的a做了一次赋值

3、范围(要当心移位运算时发生范围溢出)

实战问题:

位基本操作 PKU 2453 An Easy Problem

http://acm.pku.edu.cn/JudgeOnline/problem?id=2453

我们还是从简单的题目开始看

首先是PKU的2453,这题本身就是设计就是很清楚的位运算

题目的大意是给一个数I要求出一个比I大的最小值,这个值二进制的表示1的数量要和I中1的数目相同。

那么首先我们可以想到的方法是,对相同的I的排列数,不用通同位运算得解,但是效率应该不会很高,如果要提高效率或者可以研究一个1的位置变换的规律,比如当I中只有一个1的时候,这时,答案只需将I左移一位:1à2 ,4à8

其次,我们想到的肯定是位操作来解决,运用位操作的高效应该可以用枚举来完成此题。

我们可以很容易地想到这样的枚举方式:

for (i=I+1;;i++) If (i中1的个数和I相同) break;

print(i);

这样的话要处理的之剩下,如何判断i中1的个数是否和I相同

求出i中1的个数:

首先题目最大的数为:1000000 得到的2进制有20位之多

可以确定的是,最大的这个数的解不会超出20位

写一个函数传入n:

const int M = 20;

int cnt = 0;

for (i=0;i<=M;i++) if ((n>>i)&1) cnt++;

return cnt;

这样便可以求出一个数n中1的个数了。

具体AC代码:(C++)

#include <iostream>

using namespace std;

const int M = 21;

int num(int n)

{

       inti,cnt = 0;

       for(i=0;i<=M;i++)

              if((n>>i)&1)

                     cnt++;

       returncnt;

}

int main()

{

       inti,I;

       while(scanf("%d",&I)!=EOF,I)

       {

              intnumI = num(I);

              for(i=I+1;;i++)

                     if(num(i) == numI)

                            break;

              printf("%d\n",i);

       }

       return0;

}

以上代码的时间复杂度大约为O(nm)

虽然AC了,但这题光凭枚举效率一定是不高的,看一下运行的情况:

2453 Accepted 204K 219MSC++ 342B

为了提高时间效率,其实这道题还是可以找到规律的

其解法为:从右向左,找到第一个“01”以便进位

那么我们把01里的1向前进一位,变成10,它后面的1要移到最右边

实现:我们可以从右向左遍历这个数,找到第一个’01’,进位后把后面的1移到最右边

这样的一个算法,可以写成O(n+m)也可以优化到O(n)

我用优化的方法写了一下:

具体AC代码:(C++)

#include <iostream>

using namespace std;

const int M = 21;

int main()

{

    inti,j,k,I;

   while (scanf("%d",&I)!=EOF,I)

    {

         int numOfRZero = 0;

         int flag;

         for (i=0;i<=M;i++)

         {

             if ((I>>i)&1)

             {

                  if(!((I>>(i+1))&1))

                  {

                      I+=(1<<i);

                      flag = i;

                      break;

                  }

             }

             else

                  numOfRZero++;

         }

         int cleaner = 0xffffff; //用来将后面的几位清零

         int clean = 0xffffff;

         cleaner <<=  flag;

         cleaner &= clean;

         I &= cleaner; // 将后面的几位清零

         clean >>= (24-(flag-numOfRZero));

         I |= clean;

         printf("%d\n",I);

    }

   return 0;   

}

运行结果:

2453 Accepted 204K 0MSC++ 1120B

终于0MS AC ,但这样大家就满足了吗?!

我不得不提,我们应该清楚一点:其实位运算的最高境界是:无遍历

也是就是在常数的时间效率里得解。

高手AC CODE:(C)

#include <stdio.h>

int main()

{

    intn,x;

   while(scanf("%d",&n),n)

    {

        x=n&-n;

       printf("%d\n",n+x+(n^n+x)/x/4);

    }

       return0;

}

两行代码,无遍历,0MS,两变量

一切搞定,我想这就是最高的境界吧

虽然我不能想到如此优的解法,

但这两句代码,将对我们的位操作学习,有无尽的启发!

位压缩技术 PKU 2454 Set Operation

http://acm.pku.edu.cn/JudgeOnline/problem?id=2454

OK,唾沫横飞地分析完一道题中位运算的基本操作

现在我们可以深入地学习位压缩存储技术

PKU2454 这是一道看似简单的题目,但是庞大的测试数据将是我们的第一个BOSS

题意:给定一定数目的集合,然后每给出一对数据i,j判断是否它们在某个集合中同时出现过。(这样的题最好不要给中大描述,他们一定也会用一句话硬要把它说表达出来)。

对于这样的题目,我们的第一反应是,存入数组,模拟一个个集合,然后查找。

但是一看数据量很庞大,于是我用了HASH技术,把每个集合的查找时间降低到O(1),

这样对于一组数据的总体时间只要O(n),n的最坏情况为1000,还是可以接受的,以下是我的代码:

#include<iostream>

usingnamespace std;

bools[1005][10005];

intmain()

{

       int i,j,k,m,t,temp,n;

       while (scanf("%d",&n)!=EOF)

       {

              memset(s,0,sizeof(s));

              for (i=0;i<n;i++)

              {

                     scanf("%d",&t);

                     for (j=0,k=0;j<t;j++)

                     {

                            scanf("%d",&temp);

                            s[i][temp] = 1;   //标记这个数已用

                     }

              }

              //---------------------------------------------

              scanf("%d",&m);

              int a,b;

              for (i=0;i<m;i++)

              {

                     scanf("%d%d",&a,&b);

                     for (j=0;j<n;j++)

                     {

                            if(s[j][a]&& s[j][b])

                            {

                                   printf("Yes\n");

                                   break;

                            }

                     }

                     if (j == n)

                            printf("No\n");

              }

       }

       return 0;

}

运行结果:

2443 Time Limit Exceeded C++ 616B

我已经优化过了还超时!我哭~~!看了一下Discuss,有人说是位压缩技术

但是这道题怎么想也想不出可以比我以上代码的更优的位压缩方式

后来受高人启发,压缩的不是数据而是位置,即压缩集合编号。

这样不但压的空间,对查找到的时间也可以在常数时间内完成

再运用位操作中的与运算,就可以把时间降到O(n/(sizeof(int)*8))了                               

AC CODE:

//为了快速解题,下面的代码我采用了中式英文注释

#include<iostream>

usingnamespace std;

ints[10005][33];

intmain()

{

       int i,j,k,m,t,temp,n;

       int a,b;

       while (scanf("%d",&n)!=EOF)

       {

              memset(s,0,sizeof(s));

              for (i=0;i<n;i++)

              {

                     scanf("%d",&t);//num of sets

                     for (j=0;j<t;j++)

                     {

                            scanf("%d",&temp);//element of each set

                            //calculate thepostion of the element

                            int bitPos =(1<<(i%32)); //the position of 1 means the bit position

                            if((s[temp][i/32]&bitPos) == 0) //if elements repeat ,drop it ..else save it

                                   s[temp][i/32]+= bitPos;

                     }

              }

              scanf("%d",&m);

              for (i=0;i<m;i++)

              {

                     scanf("%d%d",&a,&b);

                     // seach sets use bit op

                     for (j=0;j<33;j++) //nowwe just need to seach 32 elems

                     {

                            if (s[a][j] &s[b][j]) // if there are the same pos 1 in two set num

                                   break;                      //       itmeans these two in the same set

                     }

                     if (j == 33)printf("No\n");

                     elseprintf("Yes\n");

              }

       }

       return 0;

}

2443Accepted 1496K 782MS C++ 1001B

位操作在BFS中的优化

这项技术我是从“黑白棋游戏”中学的,大家可以看一下题报告:

http://blog.csdn.net/jqandjq/archive/2008/12/02/3430846.aspx

后来发现的很多搜索和枚举都可以用它来优化,不过对思维要求是要很清晰。

PKU 2965 The Pilots Brothers'refrigerator

http://acm.pku.edu.cn/JudgeOnline/problem?id=2965

题目的大意是:有一扇门,上面有16(4*4)个开关,要把门打开就要让16个开关都为打开的状态。现在给你的初始状态至少有一个是关闭的,你改变一个开关的状态时,相应的行和列的开关都会被改变,那么问怎么做才能使开关全开。

这是一题再经典不过的搜索题了,不过我们要用位运算来优化此题

我们可以用一个INT型来表示一扇门的状态,1为关门0为打开。

当改变一个状态的时候,只要对相应行列的状态做一个XOR运算就OK了

剩下的就全看搜索的功底了

这道题出得很好,主体的要求需要我们用BFS

而输出的时候为了打印路径,需要我们用DFS(用一般递归就OK)

两种搜索集合在一道题里了。

AC CODE:

#include<iostream>

#include<queue>

usingnamespace std;

boolbfs();

intchange[4][4] =

{

       0xf888,0xf444,0xf222,0xf111,

       0x8f88,0x4f44,0x2f22,0x1f11,

       0x88f8,0x44f4,0x22f2,0x11f1,

       0x888f,0x444f,0x222f,0x111f

};

structNode

{

       int x,y;

       int last;

       int step;

};

intstart,temp;

Nodestate[70000];

boolused[70000];

int flag;

voidfinds(int n)

{

       if (state[n].last == -1)

              return;

       finds(state[n].last);

       printf("%d%d\n",state[n].x+1,state[n].y+1);

       return;

}

intmain()

{

       int i,j,k,m,n,t;

       char str[4][4];

       start = 0;

    char c;

    int num = 32768;

              for (i = 0; i < 4; ++i)

              {

                     for (j = 0; j < 4; ++j)

                     {

                            scanf("%c",&c);

                            if (c == '+')

                                   start += num;

                            num >>= 1;

                     }

                     getchar();

              }

              //-------------------------------------------------------

              memset(used,0,sizeof(used));

              flag = 0;

              bfs();

        printf("%d\n",state[0].step);

        finds(0);

       return 0;

}

boolbfs()

{

       int i,j,t=-1;

       int head = 0;

       int tail = 1;

       queue <int> Q;

       Q.push(start);

       used[start] = 1;

       state[start].last = -1;

       state[start].step = 0;

       while (!Q.empty() && t!=0)

       {

              temp = Q.front();

              Q.pop();

              for (i=0;i<4;i++)

              {

                     for (j=0;j<4;j++)

                     {

                            t =temp^change[i][j];

                            if (!used[t])

                            {

                                   Q.push(t);

                                   state[t].last= temp;

                                   state[t].step= state[temp].step + 1;

                                   state[t].x =i;

                                   state[t].y =j;

                                   used[t] = 1;

                                   if (t == 0)

                                   {

                                          break;

                                   }

                            }

                     }

              }

       }

       return 0;

}

相关例题:

PKU:2453 2443 1753 2965

(2081 2050 1324 3460附加网摘题目,不作重点要求)

.

参考网页及文献:

按位左右移位运算符

http://www.nshen.net/blog/article.asp?id=437

按位与运算符

http://wqp1977wqp.blog.163.com/blog/static/119448722007454246783/

按位或运算符

http://geniusvic.blog.hexun.com/21245303_d.html

按位非运算符

http://doc.51windows.net/jscript5/?url=/jscript5/html/jsoprbitwisenot.htm

A bit of fun: fun with bits  By bmerry

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=bitManipulation

Victordu PKU 2443 位运算

http://www.cppblog.com/Victordu/archive/2008/02/18/42887.aspx