位运算
位运算
程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。比如,and运算本来是一个逻辑运算符,但整数与整数之间也可以进行and运算。举个例子,6的二进制是110,11的二进制是1011,那么6 and 11的结果就是2,它是二进制对应位进行逻辑运算的结果(0表示False,1表示True,空位都当0处理)。
各种位运算的使用
=== 1. & 运算 ===
and 运算通常用于二进制取位操作, 例如一个数 and 1的结果就是取二进制的最末位。 这可以用来判断一个
整数的奇偶, 二进制的最末位为 0 表示该数为偶数, 最末位为 1 表示该数为奇数.
=== 2. |运算===
or 运算通常用于二进制特定位上的无条件赋值, 例如一个数 or 1 的结果就是把二进制最末位强行变成 1。
如果需要把二进制最末位变成 0, 对这个数 or 1 之后再减一就可以了, 其实际意义就是把这个数强行变成最接
近的偶数。
=== 3. ^ 运算===
xor 运算通常用于对二进制的特定一位进行取反操作, 因为异或可以这样定义: 0 和 1 异或 0 都不变, 异或 1
则取反。
=== 4. ~ 运算 ===
~ 运算的定义是把内存中的0和 1 全部取反。 使用 ~ 运算时要格外小心, 你需要注意整数类型有没有符
号。
=== 5. << 运算===
a << b就表示把a转为二进制后左移b位 ( 在后面添b个0)。
=== 6. >> 运算===
和 >> 相似, a >> b 表示二进制右移 b 位( 去掉末 b 位), 相当于 a 除以 2 的 b 次方( 取整)。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
分割线(上面全是粘的)
在使用位运算的时候,需要注意的一点是他的优先级
上面这个图是在别的博客里找到=。= 好难记。。。
其实只要记住这几点就差不多了:
1.取非比较快,仅次() .
2.其余需要两个变量的位运算比算术运算(如+ - * / )要低,比赋值运算(=),逻辑运算要高(||, &&)
3.左移右移比较调皮,跳到关系运算(> < <= == !=)上面
如果还是不好记。。那就括号大法好
位运算这么麻烦,为什么还要用他呢,因为他的速度特别快,可以用位运算来实现一些功能
比较常用的几个功能:
a乘以2除以2:<< >>
判断奇偶性:a&1,为零为偶,否则奇数
交换两个数:a^=b^=a^=b;(可以不借助别的变量)
判断两个数是否相同:a^b==0则相同,否则不同(可以快速从只有一个数出现一次其余都出现偶数次的一串数里找到这个数,
1 1 2 3 3 3 3 4 4 ,从头异或一遍,就可以得到2)其实异或还有更大作用的用法,下面会提到
去掉二进制最后一个1:i-=i&-i,也可以写作i=i&(i-1),在树状数组里会用到
下面举几个简单基础的可能会用到位运算的常见算法和有趣的题:
1.首先肯定是快速幂,实用且简单,位运算则大大节省了时间(其实因为快速幂本身就很快,不选择位运算一般的题也能ac)
下面是代码:
- <span style="font-family:SimHei;font-size:14px;">#include<stdio.h>
- int main()
- {
- int n,a;
- scanf("%d %d",&n,&a);
- int t=n,ans=1;
- while(a)
- {
- if(a&1)
- ans*=t;
- t*=t;
- a>>=1;
- }
- printf("%d\n",ans);
- return 0;
- }</span>
像这些比较有名的算法,只说用法,就不说原理了,
还有快速幂延伸的矩阵快速幂。。。
2.
给定一个数组A, 长度为n,求下面这段程序的值
ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
ans ^= A[i] + A[j];
多组测试数据,第一行一个整数T,T<=20,表示数据的组数,每组数据一行包含n, m, z, l; A[1] = 0; A[i] = (A[i-1] * m + z) mod l; 1 <= n, m, z, l <= 5 * 10^5
每组数据输出Case #x: ans 其中x代表第几组测试数据,从1开始,ans代表程序中的ans.
2
3 5 5 7
6 8 8 9
Case #1: 14
Case #2: 16
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
水题一秒思考分界线
这道题不是很难,但也挺有意思的,重点考察对异或的理解,直接模拟肯定是过不了的,
A[i] + A[j]=A[j] + A[i];所以除了主对角线上的项只异或了一遍外,其余都异或了两遍,异或两遍不就为零了,说以直接把主对角线上的乘二异或起来就好了
- <span style="font-family:SimHei;font-size:14px;">#include<stdio.h>
- int main()
- {
- long long int t,ans,sum,a;
- int n,m,i,z,l;
- scanf("%lld",&t);
- for(int j=1;j<=t;j++)
- {
- sum=0;
- ans=0;
- scanf("%d %d %d %d",&n,&m,&z,&l);
- a=0;
- for(i=1;i<n;i++)
- {
- a=(a*m+z)%l;
- ans^=(a*2);
- }
- printf("Case #%d: %lld\n",j,ans);
- }
- }</span>
给你一个整数数列,保证只有一个数出现过奇数次,输出它。
多组测试数据。 每组测试数据第一行为一个整数n,代表该数列元素个数。(1 <= n <= 500000) 第二行为n个整数ai,以空格隔开。(-1000000 <= ai <= 1000000)
输出一行表示这个出现奇数次的数。
5
2 3 2 3 1
7
6 6 6 2 6 6 6
1
2
这个题更水,懂上面那道题了,这个还不秒切?
全部异或起来就出答案了,因为异或符合交换律,而别的数异或0等于本身,出现偶数次的数会全部异或成0,奇数次的数自然也就出来了
- #include<stdio.h>
- int main()
- {
- int a,b;
- int n;
- while(scanf("%d",&n)!=EOF)
- {
- scanf("%d",&a);
- for(int i=1;i<n;i++)
- {
- scanf("%d",&b);
- a^=b;
- }
- printf("%d\n",a);
- }
- return 0;
- }
4.
数数字 2
输入一些数字,int范围内,大部分数字都出现了三次,只有一个数字出现了一次,输出这个数字。
第一行是数字的个数n,n < 2000000,接下来每行一个数字。
输出出现了一次的数字
4
1
1
1
3
3
这个要比上面的复杂一些,出现三次和出现一次,都是奇数次,异或起来好像没什么区别啊=。=看了半天,想了半天丝毫没有思路,没办法,只能搜题解了
因为十进制的数字在计算机中是以二进制的形式存储的,所以数组中的任何一个数字都可以转化为类似101001101这样的形式,int类型占内存4个字节,也就是32位。那么,如果一个数字在数组中出现了三次,比如18,二进制是10010,所以第一位和第四位上的1,也都各出现了3次。
因此可以用ones代表只出现一次的数位,twos代表出现了两次的数位,xthrees代表出现了三次的数位。
- public int singleNumber(int[] A) {
- int ones=0;
- int twos=0;
- int xthrees=0;
- for(int i = 0;i <A.length;i++){
- twos ^= (ones&A[i]);
- ones ^= A[i];
- xthrees = ~(ones&twos);
- twos &= xthrees;
- ones &=xthrees;
- }
- return ones;
- }
不过说实话,我没怎么看懂。。。。不过后来从某飞君学到了一个更简单的做法,按位记录每一个数那个位的出现的次数,最后把这些位的次数对三取余,再转为十进制就是要求的数。虽然没有用到位运算,但这是一种按位去考虑的方法,值得一记。
- #include<stdio.h>
- #include<string.h>
- int main()
- {
- int dight[40],a,b=0;
- int n,i;
- memset(dight,0,sizeof(dight));
- scanf("%d",&n);
- while(n--)
- {
- scanf("%d",&a);
- i=0;
- while(a)
- {
- dight[i++]+=a%2;
- a/=2;
- }
- }
- int t=1;
- for(i=0;i<32;i++)
- {
- dight[i]%=3;
- b+=dight[i]*t;
- t*=2;
- }
- printf("%d\n",b);
- return 0;
- }
这个复杂度是nlogn,所以sort也能做,仅供参考
5.nim博弈问题:
通常的Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
对于一个Nim游戏的局面(a1,a2,...,an),它是必赢状态当且仅当a1^a2^...^an=0,其中^表示异或(xor)运算。
题目链接:HDU 1849
Rabbit and Grass
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 3767 Accepted Submission(s): 2844
说是下棋,其实只是一个简单的小游戏而已,游戏的规则是这样的:
1、棋盘包含1*n个方格,方格从左到右分别编号为0,1,2,…,n-1;
2、m个棋子放在棋盘的方格上,方格可以为空,也可以放多于一个的棋子;
3、双方轮流走棋;
4、每一步可以选择任意一个棋子向左移动到任意的位置(可以多个棋子位于同一个方格),当然,任何棋子不能超出棋盘边界;
5、如果所有的棋子都位于最左边(即编号为0的位置),则游戏结束,并且规定最后走棋的一方为胜者。
对于本题,你不需要考虑n的大小(我们可以假设在初始状态,棋子总是位于棋盘的适当位置)。下面的示意图即为一个1*15的棋盘,共有6个棋子,其中,编号8的位置有两个棋子。
大家知道,虽然偶尔不够浪漫,但是Rabbit和Grass都是冰雪聪明的女生,如果每次都是Rabbit先走棋,请输出最后的结果。
- #include<stdio.h>
- int main()
- {
- int ans,a,m;
- while(~scanf("%d",&m))
- {
- ans=0;
- if(m==0)
- break;
- while(m--)
- {
- scanf("%d",&a);
- ans^=a;
- }
- if(ans==0)
- printf("Grass Win!\n");
- else
- printf("Rabbit Win!\n");
- }
- return 0;
- }
只介绍用法,至于为什么要用异或,可以搜nim博弈的相关博客
6.树状数组
树状数组是用于求区间和,同时支持单点更新
优点就是代码长度短,可以求解一些线段树的题
模板:
- #include<stdio.h>
- #define max_n 1000
- int bit[max_n+1],n;
- int sum(int x)
- {
- int sum=0;
- while(x>0)
- {
- sum+=bit[x];
- x-=x&-x;
- }
- return sum;
- }
- void add(int x,int a)
- {
- while(x<=n)
- {
- bit[x]+=a;
- x+=x&-x;
- }
- return ;
- }
- int main()
- {
- int x,i,a;
- scanf("%d",&n);
- for(i=1;i<=n;i++)
- {
- scanf("%d",&a);
- add(i,a);
- }
- scanf("%d",&x);
- printf("%d\n",sum(x));//前x个数的和
- scanf("%d",&a);
- add(x,a);//单点更新
- printf("%d\n",sum(x));
- return 0;
- }
7.判断一个数x是不是2的某次方
x&(x-1)==0?YES : NO;
总之,位运算既骚气又高效,如果能够灵活掌握对自己将会是一个很大的提高