一些常用的类型的大概范围
4个字节的int类型大概在21亿左右(-2147483648~2147483647)即-2^31到(2^31)-1。
当任意两个数值做加减法运算,都等价于在32位补码下做高位不进位的二进制加减法运算,发生算术溢出时,32位无符号整数相当于对2^32取模,这也解释了有符号整数算术上溢时出现负数的现象。
0x3f3f3f3f是一个很有用的数值。它是满足以下两个条件的最大整数
1.整数的两倍不超过0x7f ff ff ff(0111……111),即int能表示的最大整数。
2.整数的每8位(每个字节)都是相同的。
memset(a,0x3f,sizeof(a))作用是将0x3f填充到数组a的每个字节上,而int有四个字节,所以用memset只能赋值出“每8位都相同”的int。
其实用 0x7f 7f 7f 7f也能初始化为最大值,不过我们需要把一个数组中的数值初始化成正无穷时,为了避免加法算术上溢或者繁琐的判断,我们经常用0x 3f 3f 3f 3f来初始化。
常见的取值范围
unsigned int 0~4294967295
int -2147483648~2147483647 (10位)
unsigned long 0~4294967295
long -2147483648~2147483647
long long的最大值:9223372036854775807 (19位)
long long的最小值:-9223372036854775808
unsigned long long的最大值:1844674407370955161
__int64的最大值:9223372036854775807
__int64的最小值:-9223372036854775808
unsigned __int64的最大值:18446744073709551615
移位运算
左移(低位以0填充,高位越界后舍弃)
例 1<<n = 2^n n<<1=2n
算术右移(高位以符号位填充,低位越界后舍弃)
注意:算术右移与整数/2的区别:算术右移等于除以2向下取整(-3>>1=-2)而整数/2等于相当于去掉小数部分(-3/2=-1)
逻辑右移(高位以0填充,低位越界后舍弃)
二进制状态压缩:是指将一个长度为m的bool数组用一个m位二进制整数表示并存储的方法。
操作 | 运算 |
---|---|
取出整数n在二进制表示下的第k位 | (n>>k)&1 |
取出整数n在二进制表示下的第0~k-1位(后K位) | n&((1<<k)-1) |
把整数n在二进制表示下的第K位取反 | nxor(1<<k) |
对整数n在二进制表示下的第k位赋值1 | n or (1<<k) |
对整数n在二进制表示下的第k位赋值0 | n&(~(1<<k)) |
异或的作用
1.成对变换:
通过计算可以发现,对于非负整数n:
当n为偶数时,n xor 1 等于n+1;
当n为奇数时,n xor 1 等于n-1;
这一结论常用于图论的邻接表中边集的存储。在具有无向边(双向边)的图中一对正反方向的边分别存储在邻接表数组的第n与n+1位置,(其中n为偶数)就可以通过异或来获得当前边(x,y)反向的边(y,x);
2.lowbit运算:定义为非负整数n在二进制数表示下“最低位的1及其后边所有的0”构成的数值,例:n=10(1010),lowbit(1010) = (10)
推导过程:设n>0,n的第K位是1,第0~k-1位都是0。
1.先把n取反,此时第K位变成0,第0~k-1位都是1,
2.再令n=n+1,此时因为进位,第K位变为1,第0~k-1位都是0.
所以n&(~n+1)仅有第k位为1,其余位为0,而在补码表示下。~n=-1-n,因此
lowbit(n) = n&(~n+1) = n&(-n);
例:1010-----0101------0110-----(1010)&(0110)=(0010)
下面的一个小技巧可以快速的知道二进制数中第几位是1。
思想:我们预处理一个数组H。令H[2^k] = k;如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include<iostream> #include<algorithm> using namespace std; const int N = 1<<20; int h[N+1]; int main(){ int n; for(int i=0;i<=20;i++) h[1<<i] = i; while(cin>>n){ //对多次询问进行求解 while(n>0){ cout<<h[n&-n]<<" "; n-=n&-n; } } cout<<endl; return 0; }
稍微复杂但效率更高的方法是建立一个长度为37的数组H,令H[2^k mod 37] = k,这里利用了一个小的数学知识:存在0<=k<=35 2^k mod 37 互不相等,且恰好取遍1-36;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include<iostream> #include<algorithm> using namespace std; int h[37]; int main(){ int n; for(int i=0;i<36;i++) h[(1ll<<i)%37] = i; while(cin>>n){ while(n>0){ cout<<h[(n&-n)%37]<<" "; n-=n&-n; } cout<<endl; } return 0; }
题目一:给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。
输入样例:
5
1 2 3 4 5
输出样例:
1 1 2 1 2
方法一:这里直接使用x&1 得到最低位,然后判断最低位是否是1,然后再用移位运算符,再将最低位给抹掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#include<iostream> #include<algorithm> using namespace std; int main(){ int n; cin>>n; for(int i=0;i<n;i++){ int x ,count; cin>>x; count = 0; while(x){ if(x&1) count++; x=x>>1; } cout<<count<<" "; } return 0; }
方法二:第二种方法使用了lowbit方法,每次循环将最低位的1至最后的0所组成的二进制 去掉,当数为0时,就是循环结束条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include<iostream> #include<algorithm> using namespace std; int main(){ int n; cin>>n; while(n--){ int x,count; cin>>x; count = 0; for(int i=x;i;i-=i&-i) count++; cout<<count<<" "; } return 0; }
题目二:求 a 的 b 次方对 p 取模的值。
数据范围
0≤a,b,p≤10^9
数据保证 p≠0
输入样例:
3 2 7
输出样例:
2
题目思路:如果我们直接求a^b%p 这样肯定超过了限制的时间。我们可以这样想,任何一个整数都可以化成一个二进制数,我们先将b表示成这样的形式(假设b在二进制表示下有K位)
b=c(k-1)*2^(k-1) + c(k-2)*2^(k-2) + …+ c(0)*2^(0)
于是可以得到
a^b = a^(c(k-1)*2^(k-1)) * a^(c(k-2)*2^(k-2) ) *…* a^(c(0)*2^(0) )
例:b = 10 = (1010) ----- a^10 = a^(1010) = a^8 *a^2
这样就可以将o(n)的复杂度变成o(logn)的复杂度.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include<iostream> #include<algorithm> using namespace std; int main(){ int a,b,p,sum; cin>>a>>b>>p; sum = 1%p; //当a,0,p时,当b=0时, while(b){ //先将b的最低位提取出来,如果是1, 就将结束乘a^(b&1) if(b&1) sum = 1ll*sum*a%p; //乘1ll是为了防止溢出int的范围. a = 1ll *a*a%p; //最低位的a是a^1 次最低位 a 是a^2 再次最低位 a 是a^4 依次类推,所以累乘. b=b>>1; //将最低位抹去 } return 0; }
题目二: 求 a 乘 b 对 p 取模的值。
数据范围
1≤a,b,p≤10^18
输入样例:
3
4
5
输出样例:
2
方法一:
题目思路:由于a,b,p都是10^18级别,而long long 最多可以表示19位十进制数,根本就不可能直接ab再求模,只能类似用快速幂的思想,b可以表示为:
b=c(k-1)*2^(k-1) + c(k-2)*2^(k-2) + …+ c(0)*2^(0)
然后ab= a*(c(k-1)*2^(k-1)) + a*(c(k-2)*2^(k-2) ) +…+ a*(c(0)*2^(0) )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include<iostream> #include<algorithm> using namespace std; int main(){ long long a,b,p,sum; cin>>a>>b>>p; sum = 0; while(b){ if(b&1) sum = (sum+a)%p; a =a*2%p; //a最大是18位十进制数,而long long最大可以表示19位十进制数,所以不会越界. b>>=1; } cout<<sum<<endl; return 0; }
方法二:
题目思路:利用ab mod p = ab - ((下取整)ab/p)p;
首先,当a,b<p时,ab/p下取整以后也一定小于p,所以我们可以用浮点数执行ab/p的运算,而不用关心小数点之后的部分,正好是我们所需要的取整的部分。
另外,虽然ab和 ((下取整)ab/p)p可能很大,但是二者的差一定在0~p-1之间,所以我们只关心他们的较低位即可,所以我们用long long来存储ab和 ((下取整)a*b/p)*p各自结果。整数运算溢出相当于舍弃高位。正好也符合我们的要求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include<iostream> #include<algorithm> using namespace std; int main(){ long long a,b,p,sum; cin>>a>>b>>p; sum = 0; a = a%p;b = b%p; long long c = (long double) a*b/p; sum =a*b-c*p; if(sum<0) sum+=p; else if(sum>=p) sum-=p; cout<<sum<<endl; return 0; }
题目三: 最短Hamilton路径:给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
数据范围
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个点的全排列,计算路径长度取最小值,时间复杂度o(n*n!),如果使用二进制状态压缩dp可以优化至o(n^2*2^n);我们可以使用一个n位二进制数,当第i位为1,表示这个点已经被经过,反之则没有经过。我们还可以使用F[i,j] (0<=i<2^n 0<=j<n) 表示“点被经过的状态”对应的二进制数,且目前处于点j时的最短路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27#include<iostream> #include<algorithm> #include<string.h> using namespace std; const int N =20; const int M = 1<<N; int w[N][N]; int f[M][N]; 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; //即只经过点0,目前处于起点0,最短路长度为0 for(int i=1;i<1<<n;i++) //n位二进制,有2^n种状态,一次一次遍历 for(int j=0;j<n;j++) if(i>>j&1) //当i的第j位为1, 即当前走到了j这个点。 for(int k=0;k<n;k++) if((i^1<<j)>>k&1) //我们就去掉这个j这个点,然后遍历K,如果K为1,则说明可以从k点到j点, f[i][j] = min(f[i][j],f[i^1<<j][k]+w[k][j]); //判断是从K点到j点的距离小,还是直接到小,选择一个最小的值。 cout<<f[(1<<n)-1][n-1]<<endl; return 0; }
优化代码:上面的方法是,遍历状态,然后再通过一次一次循环来找到j和k,而我们可以看到,状态中包含了j和k,所以我们可以用位运算来提取这些有价值的j和k,不像上面的方法一样,进行循环,浪费时间。从漫无目的的循环到有目的提取,减少循环次数,提高效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38#include <stdio.h> #include <string.h> const int N = 20; const int M = 1 << N; int n; int w[N][N]; int f[M][N]; int log_2[M >> 1]; // 存 1 到 M / 2 中,所有 2 的整次幂的数以 2 为底的对数 int main() { scanf("%d", &n); for (int i = 0; i < n; i ++ ) for (int j = 0; j < n; j ++ ) scanf("%d", &w[i][j]); for (int i = 0; i < 20; i ++ ) // 预处理 log_2 log_2[1 << i] = i; memset(f, 0x3f, sizeof f); f[1][0] = 0; for (int state = 1; state < 1 << n; state ++ ) if (state & 1) for (int t = state; t; t &= t - 1) // 只枚举 state 所包含的点集 { int j = log_2[t & -t]; // 将集合 t 中最小的点取出 for (int u = state ^ t & -t; u; u &= u - 1) // 只枚举 state 中去掉点 j 的点集 { int k = log_2[u & -u]; // 将集合 u 中最小的点取出 if (f[state ^ t & -t][k] + w[k][j] < f[state][j]) f[state][j] = f[state ^ t & -t][k] + w[k][j]; } } printf("%dn", f[(1 << n) - 1][n - 1]); return 0; }
一些关于移位运算符的优先级
加减(+ -) > 移位(<< >>) > 比较大小(> < == !=) > 位与(&) > 异或( xor ^) > 位或( | )
题目四: 起床困难综合症:给定 n,m 以及 n 个数和该数所对应的运算,其中运算有 与、或、异或 三种,问在所有不大于 m 的非负整数中,对给定的 n 个数都按该数所对应的运算运算一遍后,能得到得最大的值是多少。
输入样例:
3 10
AND 5
OR 6
XOR 7
输出样例:
1
题目思路:本题是让我们选择一个【0,m】之间的一个整数,然后给定的n次位运算,使结果最大。但是我们知道位运算的主要特点之一是在二进制表示下不进位。即参加位运算的各个位之间是独立无关的。所以我们可以从高位到低位,依次考虑每一位填0还是1。
x的第k位应该填1,当且仅当同时满足下列两个条件:
1.已经填好的更高位构成的数值加上1<<k以后不会超过m
2.用每个参数的第K位参与位运算,若初值为1,则n次位运算后结果为1;若初值为0,则n次位运算的结果为0。(如果该位填 1 后,所得到的数对 n 个数都运算之后,结果小于等于该位填 0 后得到的结果,那么为了让剩下能填的数更大,该位填 0)
如果不满足上述条件,要么填1会超过m的范围,要么填1不如填0更优。确定每一位后,就可以确定最后的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40#include<iostream> #include<algorithm> using namespace std; const int N =100010; int n,m; int t[N]; //用来存储输入的n个数 int op[N]; //用来存储输入的n个数的相应的运算符 char str[4]; //用来暂时存放输入的运算符 int calc(int bit,int now){ for(int i=0;i<n;i++){ int x = t[i]>>bit&1; //题目输入n个数,将每个数右移bit位再与1,提取第bit位, 再让其与这个数的运算符进行操作。 if(op[i] == 1) now|=x; if(op[i] == 2) now^=x; if(op[i] == 3) now&=x; } return now; //返回结果。 } int main(){ cin>>n>>m; for(int i=0;i<n;i++) { cin>>str>>t[i]; if(*str =='O') op[i] = 1; //当输入OR时表示成1;当输入XOR时表示成2;当输入AND时表示成3。 if(*str =='X') op[i] =2; if(*str =='A') op[i] =3; } int val =0; //当第K位填1时,但是构成的结果不能超过m;val就是用来判断是否超过m int ans = 0; //用来存储最大能表示的数值 for(int bit=29;bit>=0;bit--){ //m 最大是 10 ^ 9,log2(10 ^ 9) = 3log2(10 ^ 3) < 3 * 10 = 30,所以每次 i 从 29 往后枚举就可以了 int res0 = calc(bit,0); int res1 = calc(bit,1); if(val+(1<<bit)<=m && res0<res1) //当第bit位填1不超过m时,而且填1比填0更优时,直接填1。反之填0 val+=1<<bit,ans+=res1<<bit; else ans+=res0<<bit; } cout<<ans<<endl; return 0; }
总结:位运算非常基础,涉及移位,二进制状态压缩,位的提取,lowbit运算等相关知识。快速幂算法通过将其中一个数变成二进制的表示形式,将o(n)变成o(logN)提高效率,然后就是二进制压缩的二道题目,一个dp中使用状态压缩,节省空间,还有一个起床困难的题目,体现了二进制的特点:二进制表示下不进位,即参与位运算的各个位之间是独立无关的。
最后
以上就是敏感高跟鞋最近收集整理的关于0x01 位运算的全部内容,更多相关0x01内容请搜索靠谱客的其他文章。
发表评论 取消回复