位运算
程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。比如,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)
下面是代码:
#include
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;
}
像这些比较有名的算法,只说用法,就不说原理了,
还有快速幂延伸的矩阵快速幂。。。
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];所以除了主对角线上的项只异或了一遍外,其余都异或了两遍,异或两遍不就为零了,说以直接把主对角线上的乘二异或起来就好了
#include
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,代表该数列元素个数。(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
int main()
{
int a,b;
int n;
while(scanf("%d",&n)!=EOF)
{
scanf("%d",&a);
for(int i=1;i
4.
输入一些数字,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
不过说实话,我没怎么看懂。。。。不过后来从某飞君学到了一个更简单的做法,按位记录每一个数那个位的出现的次数,最后把这些位的次数对三取余,再转为十进制就是要求的数。虽然没有用到位运算,但这是一种按位去考虑的方法,值得一记。
#include
#include
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;
}
5.nim博弈问题:
通常的Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
对于一个Nim游戏的局面(a1,a2,...,an),它是必赢状态当且仅当a1^a2^...^an=0,其中^表示异或(xor)运算。
题目链接:HDU 1849
#include
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;
}
6.树状数组
树状数组是用于求区间和,同时支持单点更新
优点就是代码长度短,可以求解一些线段树的题
模板:
#include
#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;
总之,位运算既骚气又高效,如果能够灵活掌握对自己将会是一个很大的提高。