0x01位运算
零生一、一生二,二生三,三生万物。
文章目录
一、位运算符
1.1 相关知识点:
按位与(&):对两个操作数的每个对应位执行逻辑与操作,若某位为1:则两个操作数相应位上都为1,否则为0。
int a = 5; // 二进制表示为101
int b = 3; // 二进制表示为011
int c = a & b; // 对a和b按位与操作,结果为001(即1)
按位或(|):对两个操作数的每个对应位执行逻辑或操作,若某位为1:则两个操作数相应位上至少有一个为1,否则为0。
int a = 5; // 二进制表示为101
int b = 3; // 二进制表示为011
int c = a | b; // 对a和b按位或操作,结果为111(即7)
按位异或(^):对两个操作数的每个对应位执行逻辑异或操作,若某位为1:则两个操作数相应位上不相同,否则为0。
int a = 5; // 二进制表示为101
int b = 3; // 二进制表示为011
int c = a ^ b; // 对a和b按位异或操作,结果为110(即6)
按位取反(~):对操作数的每个位执行逻辑取反操作,将1变为0,将0变为1。
int a = 5; // 二进制表示为101
int b = ~a; // 对a按位取反操作,结果为-6(即二进制表示为111...1010)
左移(<<):将操作数的所有位向左移动指定的位数。左移运算符用来实现乘以2的幂的效果。
int a = 3; // 二进制表示为011
int b = a << 2; // 将a左移2位,结果为01100(即12)
右移(>>):将操作数的所有位向右移动指定的位数。右移运算符用来实现除以2的幂的效果。
int a = 12; // 二进制表示为1100
int b = a >> 2; // 将a右移2位,结果为0011(即3)
1.2 例题:
例题1:
蓝书上解释的十分详细,所以这里只捋一遍这个思路是如何产生的。
要求: a b m o d p a^{b} mod \ p abmod p
首先可以把b拆分成二进制:
b=5= 1 ∗ 2 2 + 0 ∗ 2 1 + 1 ∗ 2 0 1*2^{2}+0*2^{1}+1*2^{0} 1∗22+0∗21+1∗20
a b = a 1 ∗ 2 2 ∗ a 0 ∗ 2 1 ∗ a 1 ∗ 2 0 a^{b}=a^{1*2^2}*a^{0*2^1}*a^{1*2^0} ab=a1∗22∗a0∗21∗a1∗20
归根结底,我们要知道b在哪一个位置上有1,并且我们要遍历所有有1的位置。
做法:
b&1
可以求出b的最后一位是否为1
b>>1
可以将b往右移动一位,不断进行,则相当于遍历b的每一位
然后,我们只要在遍历b的时候,不断将 a a a自增, a = a 2 a=a^2 a=a2, 然后当b的这一位为1的时候,我们将此时的 a a a乘入答案即可.
#include<bits/stdc++.h>
using namespace std;
int main(){
long long a,b,p;
cin>>a>>b>>p;
long long ans=1;
while(b){
if(b&1)ans=ans*a%p;
a=a*a%p;
b=b>>1;
}
cout<<ans%p;
}
例题2:
我们要求: a ∗ b m o d p a*b \ mod\ p a∗b mod p
但是它们的取值范围: 1 ≤ a , b , p ≤ 1 0 18 1\leq a,b,p\leq 10^{18} 1≤a,b,p≤1018
所以,我们直接运算 a ∗ b a*b a∗b 或者直接运算 $(a\ mod\ p)*(b\ mod\ p)\ mod\ p $ 都是会爆longlong的
怎么做呢?
还是熟悉的二进制拆分
将b拆成2进制部分相加,然后递推用a乘以每一位再取模,操作部分与上一题相似
#include<bits/stdc++.h>
using namespace std;
int main(){
long long a,b,p,ans=0;
cin>>a>>b>>p;
while(b){
if(b&1)ans=(ans+ a)%p;
a=a*2%p;
b=b>>1;
}
cout<<ans%p;
}
二、二进制状态压缩
2.1介绍:
二进制状态压缩是一种在计算机科学中常用的技术,用于在有限的内存空间中存储和处理大量状态或组合情况:
它通过使用二进制位来表示不同的状态,并使用位运算来操作和检索这些状态。
在二进制状态压缩中,每个状态被赋予一个唯一的二进制编码,称为状态掩码。
例如,假设我们有一个集合 {A, B, C, D},其中每个元素可以处于两个可能的状态:存在或不存在。
我们可以使用一个4位的二进制数来表示这四个元素的状态,其中每一位对应一个元素,0表示不存在,1表示存在。
例如,0000表示所有元素都不存在,而1111表示所有元素都存在。
通过使用位运算,我们可以高效地对这些状态进行操作。
例如,通过与运算(AND)可以检查某个特定元素是否存在,通过或运算(OR)可以将两个状态合并,通过异或运算(XOR)、取反运算可以切换某个元素的状态。
2.2位运算符用于二进制状态压缩:
当使用二进制状态压缩时,以下是一些常用的位运算符及其在压缩状态中的应用:
- 与运算符(&):用于检查某个特定元素是否存在。通过将状态掩码与一个特定的位掩码进行与运算,可以判断该位是否为1,即元素是否存在。
- 或运算符(|):用于将两个状态合并。通过将两个状态的掩码进行或运算,可以将它们的元素组合在一起。
- 异或运算符(^):用于切换某个元素的状态。通过将状态掩码与一个特定的位掩码进行异或运算,可以将该位的状态反转。
- 取反运算符(~):用于反转整个状态掩码。通过对状态掩码进行取反运算,可以将0变为1,1变为0,从而得到状态的补集。
- 左移运算符(<<)和右移运算符(>>):用于对状态进行位移操作。左移运算符可以将状态的位向左移动,右移运算符可以将状态的位向右移动。这可以用于在状态中添加或删除元素。
这些位运算符可以结合使用,以实现更复杂的状态操作,例如计算交集、并集、补集等。它们在二进制状态压缩中起着重要的作用,使得我们可以在紧凑的存储空间内高效地表示和处理大量的状态组合。
下面是一些实例:
- 取出整数n在二进制下的第K位
(n>>k)&1
- 取出整数n在二进制表示下的0~k-1位
n&((1<<k)-1)
- 把整数n在二进制下的第k位取反后输出
n^(1<<k)
- 对整数n在二进制下的第k位赋值为1后输出
n|(1<<k)
- 对整数n在二进制下的第k位赋值位0后输出
n&(~(1<<k))
2.3例题:
例题1:
OJ:AcWing 998. 起床困难综合症 - AcWing
简述题意:
从[1,m]中取一个数,使得其经过n次变换之后得到的值最大。
每次变换有一个opt,代表:& 、| 、 ^ 三者之一
有一个参数t,t与选出来的数按opt进行位运算。
思路:
位运算的主要特点之一就是在二进制下不进位。
所以当我们把一个数 q 看成二进制,那么其每一位上面的数经过n次运算,不会影响其他的位。
而q的每一位可以填两位数:0、1
我们可以每次探讨该位填什么更划算。
我们从高位到低位开始枚举。
位数:log2(1e9) = 29…
我们可以从30位开始枚举。
如果初始值在第bite位上取1,需要满足下面的条件:
- (当前的累加的值 +1<<bite ) ≤ \leq ≤ m
- 填1能对答案做出贡献,而填0不能对答案做出贡献,
(如果填0也能做出贡献,那填0更划算,因为后面有更多的选择空间)
对于转换的函数,由于位运算在二进制的每一位上是独立的,我们只需要和参数的第bit位进行位运算就行了。
代码:
#include<iostream>
#include<vector>
#define ll long long
using namespace std;
class door1 {
public:
door1() {
}
door1(string opt, ll t) :opt(opt), t(t) {
};
string opt;
ll t;
};
ll n, m;
vector<door1> v; //门
int slove1(int i,int j) { //当前是第i位数,当前所选的数j,通过n扇门
for (int k = 0; k < v.size(); k++) {
ll q = (v[k].t >> i) & 1; //求出第i位数
if (v[k].opt == "OR") {
j = j | q;
}
if (v[k].opt == "AND") {
j= j & q;
}
if (v[k].opt == "XOR") {
j= j ^ q;
}
}
return j;
}
void slove() {
cin >> n >> m;
int bit=30;
for (int i = 0; i < n; i++) { //输入门的信息
string opt;
ll t;
cin >> opt >> t;
door1 p = door1(opt, t);
v.push_back(p);
}
ll ans = 0;
ll test = 0;
for (int i = bit; i >= 0; i--) {
int t1 = slove1(i, 1); // 判断填1, 那么经过n扇门后,会变成什么
int t0 = slove1(i, 0); //判断填0,经过n扇门后,会变成什么
if (test+(1 << i) <= m and t1==1 and t0==0) { // 如果初始位填1,通过n扇门后还是1,且填0通过n扇门后不是1,并且不超过m,那么填1赚
test += 1 << i;
ans += 1 << i;
}
else { //如果初始位置填1,通过n扇门后是1,填0. 转换后也是1,那么我们填0更赚
ans += t0 << i;
}
}
cout << ans;
}
int main() {
slove();
}
三、小tips
3.1运算符优先顺序
优先级(从左到用递减) | ||||||
---|---|---|---|---|---|---|
运算符 | 加、减 | 左移、右移 | 比较 | 位与 | 异或 | 位或 |
+/- | << 、>> | <、>、==、!= | & | ^ | | |
3.2成对变换
对于非负整数n:
- 当n为偶数时,n^1 == n+1
- 当n为奇数时,n^1==n-1
因为n不断异或,只会在两个数之间左右横跳,所以我们说其关于^1 成对变换
3.3lowbit函数
lowbit(n) 函数可以求出 n的二进制下,最后一个1代表的数。
例如 n=10 ,二进制是(1010) 。 则 lowbit(n) = 2。
具体实现:
先把n取反:~n (0101),然后再加一:(~n)+1 (0110)
会出现这样的结果:最后一个1的位置不变,而其后面的所有位都变成0,其前面的所有位都与n相反
所以我们只要&上(~n)+1 , 就可以求出n在二进制下代表的数。
在补码的表示下:~n = -1-n
所以lowbit函数的实现就是 n&(-n)
int lowbit(int n){
return n&(-n)
}