一、位运算的实质和定义
二进制是计算机的基本操作单位,而位运算就是计算机采用的基本运算方式。在计算机内部,所有数据都按照二进制的方式储存和进行处理,因此位运算在计算机科学中尤为重要。由于位运算是计算机进行处理和运算的基础,所以使用位运算有以下几点好处:
- 执行效率相比传统的加减乘除四则运算较高。
- 定义相比传统四则运算在计算机中更加明确且不容易出现歧义。
- 一些高级的数据结构和算法(比如线段树和树状数组)需要使用位运算提高运行效率和代码简洁程度。
但是,我们还是需要处理一些特殊的问题,比如运算符的优先级问题和运算的基本原理。
综上所述,位运算是计算机的基本运算,具有执行效率高的优点,但也存在优先级容易混淆、容易概念不清的缺点。
二、常见的位运算
下面我将用一张表格来说明位运算的分类和运算方式。
运算符 | 运算方式 | 举例 |
---|---|---|
与(& ) | 把两个数的每个二进制位进行逻辑与运算,只有两个位都为 1 1 1 ,结果才是 1 1 1 。 | 3 & 5 = ( 11 ) 2 & ( 101 ) 2 = ( 1 ) 2 = 1 3 \text{ }\&\text{ } 5=(11)_2\text{ }\&\text{ }(101)_2=(1)_2=1 3 & 5=(11)2 & (101)2=(1)2=1 |
或(| ) | 把两个数的每个二进制位进行逻辑或运算,两个位中只要有一个是 1 1 1 ,结果就是 1 1 1 。 | 3 ∣ 5 = ( 11 ) 2 ∣ ( 101 ) 2 = ( 111 ) 2 = 7 3 \text{ }|\text{ } 5=(11)_2\text{ }|\text{ }(101)_2=(111)_2=7 3 ∣ 5=(11)2 ∣ (101)2=(111)2=7 |
异或( ^ ) | 把两个数的每个二进制位进行异或运算, 两个位不同,结果就是
1
1
1 ,否则就是
0
0
0 ,即 1 xor 0 = 1, 1 xor 1 = 0, 0 xor 0 = 0, 0 xor 1 = 1 。 | 3 xor 5 = ( 11 ) 2 xor ( 101 ) 2 = ( 110 ) 2 = 6 3 \text{ xor } 5 = (11)_2 \text{ xor } (101)_2 = (110)_2 = 6 3 xor 5=(11)2 xor (101)2=(110)2=6 |
左移(<< ) | 把一个数左移指定位数,空白部分填充 0 0 0 。 | 3 < < 5 = ( 11 ) 2 < < 5 = ( 1100000 ) 2 = 96 3 << 5 = (11)_2 << 5 = (1100000)_2 = 96 3<<5=(11)2<<5=(1100000)2=96 |
右移(>> ) | 把一个数右移制定位数,去掉右移后被挤掉的部分。 | 5 > > 3 = ( 101 ) 2 > > 3 = 0 5 >> 3 = (101)_2 >> 3 = 0 5>>3=(101)2>>3=0 |
从上面的图标,我们可以看出,常见的位运算是与、或、异或三种运算,其中与预算和或运算的运算规则与其逻辑运算符的运算规则相近,但异或运算符的运算规则与传统的逻辑运算符有所不同。以上三种运算由于是计算机直接处理并运算,所以复杂度相对四则运算较低。
除此之外,位运算的优先级也应该注意。下面我将利用一张图表来说明位运算的优先级。
- 位运算内部:
<<
>>>
>&
>^
>|
- 总体的运算优先级:位运算符 > 逻辑运算符 > 算术运算符
如果我们不确定需要使用哪种运算符,应该加上小括号 ()
来指定运算顺序。
我们将会用下面的实例程序来说明位运算在 C++ 中的具体运用:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a = 3 & 5;
cout << a << endl; //1
a = 3 | 5;
cout << a << endl; //7
a = 3 ^ 5
cout << a << endl; //6
a = 3 << 5;
cout << a << endl; //96
a = 5 >> 3;
cout << a << endl; //0
return 0;
}
三、利用位运算进行的简单操作
- 常见的用位运算代替传统四则运算的表达式
- N < < 1 = 2 N N << 1 = 2N N<<1=2N
- N > > 1 = ⌊ N 2 ⌋ N >> 1 = \lfloor \large{\frac{N}{2}} \rfloor N>>1=⌊2N⌋
- k < < N = 2 N k k << N = 2^Nk k<<N=2Nk
- N > > k = ⌊ N 2 k ⌋ N >> k = \lfloor \large{\frac{N}{2^k}} \rfloor N>>k=⌊2kN⌋
- 查找一个正整数 N N N 的第 k k k 位是否为 1 1 1
结论:计算
N
>
>
k
&
1
N>>k\text{ }\&\text{ }1
N>>k & 1 或
N
&
(
1
<
<
k
)
N\text{ }\&\text{ }(1<<k)
N & (1<<k) 的值即可。
证明:
(1)
∵
\because
∵
N
>
>
k
=
N
2
k
N>>k=\large{\frac{N}{2^k}}
N>>k=2kN (右移 >>
的运算规则) ,
∴
\therefore
∴ 计算
N
>
>
k
&
1
N >> k \text{ } \& \text{ } 1
N>>k & 1 的值就可以判断
N
N
N 右移
k
k
k 位后的最后一位是否是
1
1
1 。
∴
\therefore
∴ 我们可以得到
N
N
N 的第
k
k
k 位是否是
1
1
1 。
(1)的代码实例:
#include <bits/stdc++.h>
using namespace std;
int n, k;
int main()
{
cin >> n >> k; //n的第k位
cout << (n >> k & 1) << endl;
return 0;
}
(2)
∵
\because
∵
1
<
<
k
=
2
k
1<<k=2^{k}
1<<k=2k (左移 <<
的运算规则),
∴
\therefore
∴
1
<
<
k
1<<k
1<<k 是一个除了第
k
k
k 位的值是
1
1
1 ,其他位上的值都是
0
0
0 的二进制数。
∴
\therefore
∴ 如果
N
N
N 的第
k
k
k 位是
1
1
1 ,
N
&
(
1
<
<
k
)
N \& (1 << k)
N&(1<<k) 的值就一定是
1
1
1 ,反之就一定是
0
0
0 。
(2)的代码实例:
#include <bits/stdc++.h>
using namespace std;
int n, k;
int main()
{
cin >> n >> k;
cout << (n & (1 << k)) << endl; //此处输出的值应该和上面的相同
return 0;
}
应用场景:
在状态压缩中,需要应用这种方法来取出某一位的值来读取相应的信息。比如一行灯的亮灭情况,记亮为
1
1
1 ,不亮为
0
0
0 ,则一种可能的状态可以表示为:
10001111
10001111
10001111 。这时如果我们想在
O
(
1
)
O(1)
O(1) 的时间复杂度内查取到第
k
k
k 盏灯的亮灭情况,则需要利用上面的办法,解决问题。
状态压缩在 状态压缩 DP 中的应用较为广泛,在一些高级数据结构中也有用处。
- 让
N
N
N 的第
k
k
k 位取反
设变化后的 N N N 为 N ′ N' N′ 。
当 N > > k & 1 = 0 N>>k\text{ }\&\text{ }1=0 N>>k & 1=0 时,
∵ \because ∵ N ′ = N + 2 k × ( 1 − 0 ) = N + 2 k N'=N+2^k \times (1-0)=N+2^k N′=N+2k×(1−0)=N+2k ,
∴ \therefore ∴ N ← N + ( 1 < < k ) N \leftarrow N+(1<<k) N←N+(1<<k) ,即N += (1 << k)
。
当 N > > k & 1 = 1 N>>k\text{ }\&\text{ }1=1 N>>k & 1=1 时,
∵ \because ∵ N ′ = N + 2 k × ( 0 − 1 ) = N − 2 k N'=N+2^k \times (0-1)=N-2^k N′=N+2k×(0−1)=N−2k ,
∴ \therefore ∴ N ← N − ( 1 < < k ) N \leftarrow N-(1<<k) N←N−(1<<k) ,即N -= (1 << k)
。
代码实例:
#include <bits/stdc++.h>
using namespace std;
int n, k;
int main()
{
cin >> n >> k;
if ((n >> k & 1) == 0)
{
n += (1 << k);
}
else
{
n -= (1 << k);
}
cout << n << endl;
// 或者去掉上面的条件判断和输出,改为:
// cout << ((n >> k & 1) == 0 ? n + (1 << k) : n - (1 << k)) << endl;
return 0;
}
四、利用位运算解决实际 OI 问题
- 题面描述
- 解题方案
先考虑第一行的 2 5 = 32 2^5=32 25=32 种状态,每一种状态中由五位 0 0 0 和 1 1 1 组成。如果第 1 1 1 行确定,下一行尝试让上一行所有数都变成 1 1 1 。到达最后一行,判断最后一行是否都是 1 1 1 并且转换次数小于等于 6 6 6 。最后,记录答案并输出最小值。
#include <bits/stdc++.h>
using namespace std;
#define ONLINE_JUDGE 1
const int N = 507;
const int dx[5] = {0, 1, 0, -1, 0}, dy[5] = {0, 0, 1, 0, -1};
int n, a[7][7], b[7][7], ans;
void solve(int x, int y)
{
for (int i = 0; i < 5; ++i)
{
int xx = x + dx[i];
int yy = y + dy[i];
a[xx][yy] ^= 1;
}
}
int main()
{
#if ONLINE_JUDGE
freopen("light.in", "r", stdin);
freopen("light.out", "w", stdout);
#endif
cin >> n;
while (n--)
{
for (int i = 1; i <= 5; ++i)
{
for (int j = 1; j <= 5; ++j)
{
scanf("%1d", &a[i][j]);
}
}
ans = 10;
memcpy(b, a, sizeof(a));
for (int i = 0; i < 32; ++i)
{
memcpy(a, b, sizeof(b));
int tot = 0;
for (int j = 1; j <= 5; ++j)
{
if (i & (1 << (j - 1)))
{
solve(1, j);
tot++;
}
}
for (int j = 2; j <= 5; ++j)
{
for (int k = 1; k <= 5; ++k)
{
if (a[j - 1][k] == 0)
{
solve(j, k);
tot++;
}
}
}
if (tot > 6) continue;
int f = 1;
for (int j = 1; j <= 5; ++j)
{
if (a[5][j] == 0)
{
f = 0;
break;
}
}
if (f)
{
ans = min(ans, tot);
}
}
ans = ans == 10 ? -1 : ans;
cout << ans << endl;
}
return 0;
}