目录
一. 位运算概述
程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算就是直接对整数在内存中的二进制位进行操作。常见的位运算包括与运算、或运算、异或运算、左移运算、右移运算等,其具体内容如下:
(1)& :与运算。若两个相应的二进制位都为1,则该位结果为1,否则为0。比如 0111 & 1011 = 0011
(2)|:或运算。若两个相应的二进制位有一个为1,则该位结果为1,否则为0。比如 0111 | 1011 = 1111
(3)^:异或运算。若两个相应的二进制位相同则为零,否则为1。比如 0111^1011 = 1100
(4)~:取反运算 。一元运算符用于将二进制位取反,即1变为0,0变为1。比如 ~0111 = 1000
(5)<<:左移运算。n<<k将n的二进制左移k位,低位补0(n*2^k)。比如 0111<<1 = 1110
(6)>>:右移运算。n>>k将n的二进制右移k位,低位被舍弃,高位(左边)补符号位,无符号数变为 n/2^k。比如 0111>>1 = 0011
注意其中~的结合方向自右至左,且优先级高于算术运算符,其余运算符的结合方向都是自左至右,且优先级低于关系运算符。所以在运算时,注意括号的利用。
二. 位运算应用
1. 集合应用
将数值n转化为二进制表示集合,每个二进制位的0和1表示该元素是否在集合中。则在集合中可以通过 & 表示交集,| 表示并集, ^ 表示对称差集,(1<<n)-1 表示n所有元素的全集。更复杂一些,s&(1<<i) 是否为0可以表示元素i是否是子集s中的一个元素(全集从0~n-1时),或者表示元素i+1是否是子集s中的一个元素(全集从1~n时)。其他集合应用技巧如下:
(1)从集合角度取n的所有子集
for(int k = (n-1)&n;k;k = (k-1)&n){
//do...
}
(2)求n个元素的所有子集
while(cin>>n)
{
for(int i = 0; i<(1<<n); i++)//枚举所有子集
{
for(int j = 0; j<n; j++)//枚举所有元素判断是否在子集里
{
if(s&(1<<i))printf("%d",i+1);
}
printf("\n");
}
}
(3)求集合或者现有子集的所有子集
while(cin>>n){
for(int i = 0;i<(1<<n);i++){//所有子集
cout<<"集合 "<<i<<" 的所有子集 :"<<endl;
for(int k = i;k>0;k = (k-1)&i){//所有 现在子集 的 所有子集
for(int j = 0;j<n;j++){//判断元素
if(k&(1<<j))cout<<j+1;
}
cout<<endl;
}
}
}
(4)求不相邻元素的所有集合
for(int i = 0;i<(1<<n);i++){
if((i>>1)&i)continue;//i里面含有相邻元素,退出
//对集合的处理
}
(5)必须含有n个指定位置子集的所有可能
while(cin>>n){
int m,k;
int sum = 0;
cin>>m;
while(m--){//求最小指定集合
cin>>k;
sum = sum|(1<<(k-1));
}
for(int i = sum;i<(1<<n);i = (i+1)|sum){//在sum基础上求所有集合
//操作
}
}
(6)必须不含有n个指定位置子集的所有可能
while(cin>>n){
int m,k;
int sum = 0;
cin>>m;
while(m--){
cin>>k;
sum = sum|(1<<(k-1));
}
sum = sum^((1<<n)-1);//^取最大集
//cout<<sum<<endl;
for(int i = sum;i>=0;i = (i-1)&sum){
cout<<i<<endl;
if(i==0)break;//一定要有,不然出现负数的位运算导致循环不断进行
}
}
(7)找出二进制中恰好含有 k个1的所有数
for (int mask = 0; mask < 1 << n; ) {
int tmp = mask & -mask;
mask = (mask + tmp) | (((mask ^ (mask + tmp)) >> 2) / tmp);
}
2. 其他应用
(1)O(1) 时间检查n是否为2的幂次
while(cin>>n){
if((n&(n-1))==0)cout<<"yes"<<endl;
else cout<<"no"<<endl;
}
(2)一个数组里所有的数字都出现两次,只有一个数字出现一次,请找出这个数字
while(cin>>n){
int sum,a;
for(int i = 0;i<n;i++){
cin>>a;
if(i==0)sum = a;
else sum = sum^a;//所有数字异或起来-〉异或的性质:自己异或自己为0,0异或n = n;
}
cout<<sum<<endl;
}
三. 异或运算
1. 异或运算规则
交换律:A^B = B^A
结合律:A^B^C = A^C^B
恒等律:A^0 = A
归零率:A^A = 0
2. 异或运算常用性质
(1)自反性:A^B^B = A^0 = A
(2)A^11111... = ~A(取反)
(3)若A^B = C,则A^C = B
(4)异或结果为0的位置表示两数字该位相同,为1的位置表示两数字该位不同
(5)若GCD(A,B) = A^B = C , 则C = A-B
3. 异或运算的应用
(1)一个数组里,所有的数字都出现两次,只有一个数字出现一次,找出这个数字
(2)一个数组里面,所有数字都出现两次,只有两个数字出现一次,找出这两个数字
4. 例题分析
一个数组里面,所有数字都出现两次,只有两个数字出现一次,找出这两个数字
假设这两个数字为A和B。那么将所有数字异或以后的结果为A^B,由于这两个数字不相同,所以异或结果肯定不为零。那么A^B的结果中,为0的位表示AB该位相同,为1的位表示AB该位不同。只要我们找到第一个为1的位(也就是AB不同的位),然后能将原数组分为两组,一组该位为1,一组该位为0。能确定的是,出现两次的数一定在同一组,AB一定在不同的组,所以我们将其中一组全部异或结果就是A或B,而A = A^B^B即可求出另一个元素。注意 n&~(n - 1) 可以找到n中最低的1位。
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000 + 7;
int num[maxn];
int getFirstOfOne(int n){
return n&~(n-1);
}
void Solve(int len){
int aXORb = 0;
for(int i = 0;i<len;i++)aXORb^=num[i];
int ans = getFirstOfOne(aXORb);
int a = 0;
for(int i = 0;i<len;i++){
if(ans&num[i]){
a^=num[i];
}
}
int b = aXORb^a;
printf("A = %d , B = %d\n",a,b);
}
int main()
{
int T;
scanf("%d",&T);
while(T--){
int n;
scanf("%d",&n);
for(int i = 0;i<n;i++){
scanf("%d",&num[i]);
}
Solve(n);
}
return 0;
}