目录
6 快速幂
7 二进制状态压缩
8 二进制优化递归
9 一道经典题
一 位运算符号
&
按位与
如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
|
按位或
两个相应的二进制位中只要有一个为1,该位的结果值为1
^
按位异或
若参加运算的两个二进制位值相同则为0,否则为1
~
取反
~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1
举例:
1000101 1000101 1000101 1000101
& 0101100 | 0101110 ~ ^ 0101110
= 0000100 = 1101111 = 0111010 1101011
移位运算:
<<
左移
用来将一个数的各二进制位全部左移n位,低位以0补充,高位越界后舍弃。
1左移n位: 1 << n=2^n(这里指2的n次方)
n左移1位: n << 1=2*n
举例:
short a=9115 0010001110011011 (9115的二进制表示)
a << 1 =18230 0100011100110110 (注意高位越界后舍弃一个0,低位填充一个0)
a << 2 = -29076 1000111001101100 (注意高位越界后舍弃两个0,低位填充两个0)
注意:左移是做乘2的运算,但这是在符号位(原码将最高位符号以0表示正,1表示负eg:0010001110011011中的最高位0就是符号位,表示是整数;而1000111001101100最高位是1,表示是负数)不变的情况下。如果符号位发生了改变,说明已经不能做乘2的运算了,否则会溢出,得到的值不是乘2的结果。
注意:short是Java中的表示,它定义的也是整形,不过是16字节(这里为了阐述符号位的改变而使用),而int是32字节
>>
右移
将一个数的各二进制位右移N位,移到右端的低位被舍弃,高位以符号位填充
n左移1位 : n >> 1=|n/2.0|
算术右移等于除以2向下取整,(-3)>> 1 = -2 ,3 >> 1 = 1
值得一提的是,“整数/2”在c++中实现为“除以2向零取整”,(-3)/ 2 = -1,3 / 2 = 1
举例:
short b = 9115 0010001110011011
(9115 >> 1) = 4557 0001000111001101(注意低位越界后舍去了一个1,高位补一0)
(9115 >> 2) = 2278 0000100011100110 (注意低位越界后舍去了两个1,高位补两0)
short c=-32766(负数符号位为1) 1000000000000010
-32766 >> 1 = -16383 1100000000000001(注意低位越界后舍弃一个0,高位补1)
-32766 >> 2 = -8192 1110000000000000(注意低位越界后舍弃0和1,高位补俩1)
二 常用技巧:
在m位二进制数中,通常称最低为为第0位,从右到左依次类推,最高位为第m-1位
(n >> k) & 1 求n二进制下的第k位是0还是1,若结果为真,是1,若结果为假,是0。因为1的二进制数中只有第0位数是1,其余位数都是0。
n^=1,,即n=n^1,能让n变成与原来相反的数(0或1)
n | (1 << k),能把n的第k位变成1
a^b^b=a
x=x&(x-1)用于消去x的最后一位
三 运算符号优先级:
加减优先级最高,位或优先级最低,从左往右优先级递减
加减 移位 比较大小 位与 异或 位或
+,- <<,>> >,<,==,!= & ^ |
四 位运算常用技巧
1 判断奇偶性
如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 的话,代表是奇数,如果是 0 则代表是偶数,所以采用位运算的方式的话,代码如下:
if(n&1){ //若n&1==1(为真)
//n是奇数
}
if(!(n&1)){ //若n&1!=1(为假)
//n是偶数
}
2 求a的b次方
本题涉及数论数论数论,在次不详细解释,有兴趣的话可以自己了解.
int pow(int a,int b){
int ans = 1;
while(b != 0){
if(b & 1 == 1){
ans *= a;
}
a *= a;
b = b >> 1;
}
return ans;
}
注意,若数据过大,应改为long long
举例: b = 13,则 b 的二进制表示为 1101, 那么 a 的 13 次方可以拆解为:
a^1101 = a^0001 * a^0100 * a^1000。
我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。
这样时间复杂度是O(logn),比库里的pow函数O(n)快
3 找处未重复的数
数组中,只有一个数出现奇数次,剩下都出现偶数次,找出出现奇数次的。
思路:两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下。也就是说,那些出现了偶数次的数异或之后会变成0,那个出现奇数次的数,和 0 异或之后就等于它本身。
#include<iostream>
using namespace std;
int main(){
int a[9]={4,3,2,2,2,2,5,4,3};
int ans=0;
for(int i=0;i<9;i++){
ans^=a[i];
}
cout<<ans;
return 0;
}
4 用O(1)时间检测整数n是否是2的幂次.
思路:
- n如果是2的幂次,则:
1.n>0
2.n的二进制表示中只有一个1
一位n的二进制表示中只有一个1,所以使用n&(n-1)将唯一的一个1消去。
如果n是2的幂次,那么n&(n-1)得到结果为0,即可判断。 - eg: 8的二进制位1000,7的二进制位0111,8&7==0,所以8是2的幂次
-
#include<iostream> using namespace std; int main(){ int n; cin>>n; if(!(n&(n-1))) cout<<"YES"; if(n&(n-1)) cout<<"NO"; return 0; }
5 计算在一个 32 位的整数的二进制表示中有多少个 1
思路:
由 x & (x-1) 消去x最后一位知。循环使用x & (x-1)消去最后一位1,计算总共消去了多少次即可
#include<iostream>
using namespace std;
int get(int num) {
// write your code here
int count =0;
while(num){
count ++;
num = num&(num-1);
cout<<num<<endl;
}
return count;
}
int main(){
int n;
cin>>n;
cout<<get(n);
return 0;
}
6 快速幂
int ksm(int a,int b)//快速幂函数
{
int ans=1;
a%=Mod;
while(b)
{
if (b&1)
ans=ans%Mod*a;
a=a%Mod*a%Mod;
b>>=1;
}
return ans;
}
7 二进制状态压缩
二进制状态压缩是指讲一个长度为m的bool数组用一个m位二进制整数表示并存储的方法。利用下列位运算操作可以实现bool数组对应下标元素的存取。
取出整数n在二进制表示下的第k位 ( n >> k ) & 1
取出整数n在二进制表示下的第0~k-1位(后k位) n & ( ( 1 << k ) - 1 )
把整数n在二进制表示下的第k位取反 n ^ (1 << k)
对整数n在二进制表示下的第k位赋值1 n | ( 1 << k )
对整数n在二进制表示下的第k位赋值0 n & ( ~ ( 1 << k ) )
最短Hamilton路径
给定一张 nn 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数 n。
接下来 n行每行 n个整数,其中第 i 行第 j个整数表示点 i 到 j 的距离(记为 a[i,j])。
对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x]并且 a[x,y]+a[y,z]≥a[x,z]。
输出格式
输出一个整数,表示最短 Hamilton 路径的长度。
数据范围
1≤n≤20
0≤a[i,j]≤10^7
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18
思路:在任意时刻如何表示哪些点已经被经过,哪些点没有哦被经过?可以使用一个n位二进制数,若其第i位(0<=i<n)为1,则表示第i个点已经被经过,反之未被经过。在任意时刻还需要知道当前所处的位置,因此我们可以使用f[i[[j](0<=i<2^n,0<=j<n)表示“点被经过的状态”对应的二进制数为i,且目前处于点j时的最短路径。总的时间复杂度是 O(n^2*2n)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=20,M=1<<N;//2的20次方,数据最坏n是20,
//一个点都有用和不用(1和0)两种情况,n=20时就是2^20
int f[M][N],w[N][N];//w表示的是无权图
//状态:1.哪些点被用过 2.目前停在哪些点上 所以用二维
//eg:点0 1 4 M=10011(第0个点存在,第1个点存在,第2,3个点不存在,第4个点存在)
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(f,0x3f,sizeof(f));//因为要求最小值,所以初始化为无穷大
f[1][0]=0;//因为零是起点,所以f[1][0]=0;
for(int i=0;i<1<<n;i++)//i表示所有的情况,i表示的集合,哪些点是否被用
for(int j=0;j<n;j++)//j表示走到哪一个点
if(i>>j&1)//i的第j位(二进制)是1
for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
if(i>>k&1)//第k位有点存在
//f[state][j]=f[state_k][k]+w[k][j];
//state_k表示state除去j后的集合,k表示停在第k个点上,state_k要包含k
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
//更新最短距离,枚举state_k这个集合里面的所有点
cout<<f[(1<<n)-1][n-1]<<endl;//表示所有点都走过了,且终点是n-1的最短距离
//位运算的优先级低于'+'-'所以有必要的情况下要打括号
return 0;
}
8 二进制优化递归
从 1∼n这 n 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数 n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好 1 个空格隔开。
对于没有选任何数的方案,输出空行。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
用一个二进制数表示选了哪些数,替代之前的a[20]数组
其中 state |= 1 << (i - 1) 代表状态的改变,选了i这个数
state ^= 1 << (i - 1) 代表状态的还原,还原没选i这个数的状态
#include <iostream>
using namespace std;
int n;
bool vis[20];
void dfs(int pos, int start, int tar, int state) {
if (pos == tar + 1) {
for (int j = 0; j < n; j ++ ) {
if (state >> j & 1) cout << j + 1 << " ";
}
cout << endl;
return ;
}
for (int i = start; i <= n; i ++) {
if (!vis[i]) {
vis[i] = true; state |= 1 << (i - 1);
dfs (pos + 1, i + 1, tar, state);
vis[i] = false;state ^= 1 << (i - 1);
}
}
}
int main() {
cout << endl;
cin >> n;
for (int i = 1; i <= n; i ++ )
dfs(1, 1, i, 0);
return 0;
}
思路:题目要求的结果是2^n个
这2^n个选择情况,对应于一个n位的2进制数的各个位取0或取1的情况。
例中n=3,即
000 -> \n
001 -> 1
010 -> 2
100 -> 3
011 -> 1 2
101 -> 1 3
110 -> 2 3
111 -> 1 2 3
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
// state 是每一个状态
for (int state = 0; state < 1 << n; state ++ ) {
// 用指针j遍历二进制数state中的每一位
for (int j = 0; j < n; j ++ ) {
if (state >> j & 1) cout << j + 1 << " ";
}
cout << endl;
}
return 0;
}
9 一道经典题
起床困难综合症:
21 世纪,许多人得了一种奇怪的病:起床困难综合症,其临床表现为:起床难,起床后精神不佳。
作为一名青春阳光好少年,atm 一直坚持与起床困难综合症作斗争。
通过研究相关文献,他找到了该病的发病原因: 在深邃的太平洋海底中,出现了一条名为 drd 的巨龙,它掌握着睡眠之精髓,能随意延长大家的睡眠时间。
正是由于 drd 的活动,起床困难综合症愈演愈烈, 以惊人的速度在世界上传播。
为了彻底消灭这种病,atm 决定前往海底,消灭这条恶龙。
历经千辛万苦,atm 终于来到了 drd 所在的地方,准备与其展开艰苦卓绝的战斗。
drd 有着十分特殊的技能,他的防御战线能够使用一定的运算来改变他受到的伤害。
具体说来,drd 的防御战线由 n扇防御门组成。
每扇防御门包括一个运算 op 和一个参数 t,其中运算一定是 OR,XOR,AND 中的一种,参数则一定为非负整数。
如果还未通过防御门时攻击力为 x,则其通过这扇防御门后攻击力将变为 x op t。
最终 drd 受到的伤害为对方初始攻击力 x 依次经过所有 n 扇防御门后转变得到的攻击力。
由于 atm 水平有限,他的初始攻击力只能为 0到 m 之间的一个整数(即他的初始攻击力只能在 0,1,…,m中任选,但在通过防御门之后的攻击力不受 m 的限制)。
为了节省体力,他希望通过选择合适的初始攻击力使得他的攻击能让 drd 受到最大的伤害,请你帮他计算一下,他的一次攻击最多能使 drd 受到多少伤害。
输入格式
第 1 行包含 2 个整数,依次为 n,m,表示 drd 有 n 扇防御门,atm 的初始攻击力为 0 到 m 之间的整数。
接下来 n 行,依次表示每一扇防御门。每行包括一个字符串 op 和一个非负整数 t,两者由一个空格隔开,且 op 在前,t 在后,op 表示该防御门所对应的操作,t 表示对应的参数。
输出格式
输出一个整数,表示 atm 的一次攻击最多使 drd 受到多少伤害。
输入样例:
3 10
AND 5
OR 6
XOR 7
输出样例:
1
样例解释
atm可以选择的初始攻击力为 0,1,…,10
假设初始攻击力为 44,最终攻击力经过了如下计算
4 AND 5 = 4
4 OR 6 = 6
6 XOR 7 = 1
类似的,我们可以计算出初始攻击力为 1,3,5,7,9时最终攻击力为 0,初始攻击力为 0,2,4,6,8,10 时最终攻击力为 1,因此 atm 的一次攻击最多使 drd 受到的伤害值为 1。
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
const int N=1e5+10;
pair<string ,int> a[N];
int n,m;
int calc(int bit,int now){// 代表现在运算的是二进制的第几位, now有两种情况,需要运算过后返回
for(int i=1;i<=n;i++){
int x=a[i].second>>bit&1;
//求参数的第k位是0还是1 首先取出第i次运算中的第几位进行二进制运算
if(a[i].first=="AND") now=now&x;
else if(a[i].first=="OR") now=now|x;
else now=now^x;
}
return now;// 现在返回的就是运算过后的二进制位
}
/*本题思路
如果该位填 1 后,所得到的数大于 m,那么该位填 !u
(已经填好的更高位构成的数值加上1<<k (k表示数的第k位),以后不超过m)
否则如果该位填 1 后,所得到的数对 n 个数都运算之后,
结果小于等于该位填 0 后得到的结果,那么为了让剩下能填的数更大,该位填 0
否则该位填 1
*/
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
char str[5];
scanf("%s%d",str,&x);
a[i]=make_pair(str,x);
}
int val=0,ans=0;
for(int bit=29;bit>=0;bit--){
//因为本题中 m 最大是 10 ^ 9,log2(10 ^ 9) = 3log2(10 ^ 3) < 3 * 10 = 30
//所以每次 i 从 29 往后枚举就可以了m最多有29个二进制位,直接从高位到低位枚举每一位
int res0=calc(bit,0);
int res1=calc(bit,1);
if(val+(1<<bit)<=m&&res0<res1) // 满足填1的条件,直接填1
val=val+(1<<bit),ans=ans+(res1<<bit);
else// 不能填1,直接填0
ans=ans+(res0<<bit);
}
cout<<ans<<endl;
return 0;
}
参考《算法竞赛进阶指南》