位运算简介
1. 基本运算
1. 按位或( ∣ | ∣)
与运算位于 C++ 运算符的第十二级。该运算把数字转成了二进制,按照每一位操作,规则如下:
- 两个都是 0 0 0,结果为 0 0 0;
- 两个都是 1 1 1,结果为 1 1 1;
- 一个是 1 1 1,一个是 0 0 0,结果为 1 1 1;
也就是说,只要有一个是
1
1
1,结果就是
1
1
1,否则就是
0
0
0。
例如:
170
∣
85
170|85
170∣85
转成二进制,分别是
10101010
10101010
10101010 和
1010101
1010101
1010101。
10101010
|01010101
---------
11111111
∴
170
∣
85
=
11111111
(
2
)
=
255
\therefore170|85=11111111(2)=255
∴170∣85=11111111(2)=255
满足交换律,结合律。
a
∣
a
=
a
a|a=a
a∣a=a
a
∣
0
=
a
a|0=a
a∣0=a
2. 按位异或(^)
按位异或位于 C++ 运算符的第十一级,英文又写成 XOR,一般数学中写成 ⊕ \oplus ⊕,同样是每一位操作,规则如下:
- 两个都是 0 0 0,结果为 0 0 0;
- 两个都是 1 1 1,结果为 0 0 0;
- 一个是 1 1 1,一个是 0 0 0,结果为 1 1 1;
也就是说,相同为
0
0
0,不同为
1
1
1,或者也可以理解为二进制中的不进位加法(因此其符号为
⊕
\oplus
⊕)。
例如:
170
⊕
85
170\oplus85
170⊕85
10101010
^01010101
---------
11111111
∴
170
⊕
85
=
11111111
(
2
)
=
255
\therefore170\oplus85=11111111(2)=255
∴170⊕85=11111111(2)=255
注意,尽管我们在数学中一般写作
⊕
\oplus
⊕,但在 C++ 程序中,依然要写成 ^。
满足交换律,结合律。
a
⊕
a
=
0
a\oplus a=0
a⊕a=0
a
⊕
0
=
a
a\oplus 0=a
a⊕0=a
3. 按位与( & \& &)
按位与位于 C++ 运算符的第十级,同样是每一位操作,规则如下:
- 两个都是 0 0 0,结果为 0 0 0;
- 两个都是 1 1 1,结果为 1 1 1;
- 一个是 1 1 1,一个是 0 0 0,结果为 0 0 0;
也就是说,只要有一个是
0
0
0,结果就是
0
0
0,否则就是
1
1
1。
例如:
170
&
85
170\&85
170&85
10101010
&01010101
---------
00000000
∴
170
&
85
=
0
\therefore170\&85=0
∴170&85=0
满足结合律,交换律。
a
&
a
=
a
a\&a=a
a&a=a
a
&
0
=
0
a\&0=0
a&0=0
4. 位左移( < < << <<)
位左移位于 C++ 运算符的第七级,其将该数的二进制每一位都左移一些位,右边的空余用
0
0
0 补齐,左边超出的位会被覆盖掉。例如:
111
(
2
)
<
<
1
=
1110
(
2
)
111(2)<<1=1110(2)
111(2)<<1=1110(2)
如果是八位二进制整数,那么有:
111
(
2
)
<
<
6
=
11000000
111(2)<<6=11000000
111(2)<<6=11000000
被覆盖掉了一个
1
1
1。
事实上,如果
x
x
x,
y
y
y 都是
z
z
z 位二进制整数,那么有
x
<
<
y
=
2
y
x
m
o
d
2
z
x<<y=2^yx\bmod 2^z
x<<y=2yxmod2z因此,位左移可以帮助我们轻松地计算
2
n
2^n
2n(即
1
<
<
n
1<<n
1<<n)。
5. 位右移( > > >> >>)
位左移位于 C++ 运算符的第七级,其将该数的二进制每一位都右移一些位,超出的位会被覆盖掉。例如:
111
(
2
)
>
>
1
=
11
(
2
)
111(2)>>1=11(2)
111(2)>>1=11(2)
事实上,
x
>
>
y
=
⌊
x
2
y
⌋
x>>y=\lfloor\frac{x}{2^y}\rfloor
x>>y=⌊2yx⌋
6. 按位取反(~)
按位取反位于 C++ 运算符的第三级,将二进制的每一位都取反,即:
- 1变成0
- 0变成1
例如 ~
1010101
(
2
)
1010101(2)
1010101(2)=
101010
(
2
)
101010(2)
101010(2)
满足结合律:~
(
(
(~
a
)
=
a
a)=a
a)=a
7. 复合运算符
二进制中的混合运算符一般有下面五个:
&
=
\&=
&=:与等于。
a
&
=
b
⇔
a
=
a
&
b
a\&=b\Leftrightarrow a=a\&b
a&=b⇔a=a&b;
∣
=
|=
∣=:或等于。
a
∣
=
b
⇔
a
=
a
∣
b
a|=b\Leftrightarrow a=a|b
a∣=b⇔a=a∣b;
^
=
=
=:异或等于。
a
a
a^
=
b
⇔
a
=
a
=b\Leftrightarrow a=a
=b⇔a=a^
b
b
b;
<
<
=
<<=
<<=:左移等于。
a
<
<
=
b
⇔
a
=
a
<
<
b
a<<=b\Leftrightarrow a=a<<b
a<<=b⇔a=a<<b;
>
>
=
>>=
>>=:右移等于。
a
>
>
=
b
⇔
a
=
a
>
>
b
a>>=b\Leftrightarrow a=a>>b
a>>=b⇔a=a>>b;
这五个运算符全部位于 C++ 运算符的第十六级。
2. 常用应用
二进制中有一些很重要的组合运算,很容易出。
1. l o w b i t lowbit lowbit
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 指的是
x
x
x 的二进制表示中,最低位的
1
1
1 所对应的值。例如,
l
o
w
b
i
t
(
12
)
=
l
o
w
b
i
t
(
1100
(
2
)
)
=
2
2
=
4
lowbit(12)=lowbit(1100(2))=2^2=4
lowbit(12)=lowbit(1100(2))=22=4
一般,比较常用的计算
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 的公式有两个,分别是
l
o
w
b
i
t
(
x
)
=
x
&
−
x
lowbit(x)=x\&-x
lowbit(x)=x&−x
和
l
o
w
b
i
t
(
x
)
=
x
&
(
x
⊕
(
x
−
1
)
)
lowbit(x)=x\&(x\oplus(x-1))
lowbit(x)=x&(x⊕(x−1))
其中公式二就出现在了 CSP-S1 2019 中。
l
o
w
b
i
t
lowbit
lowbit 最主要的用处是在树状数组中。
2. gcd \gcd gcd
gcd \gcd gcd 指的是最大公约数,一般我们用辗转相除法会写成这样:
int gcd(int a,int b)
{
if(b==0) return a;
return gcd(b,a%b);
}
然而,事实上,我们还可以写成这样:
int gcd(int a,int b)
{
while(b^=a^=b^=a%=b);
return a;
}
不用担心爆栈,看上去也更清爽。
__gcd(a,b);
……
3. 快速幂
快速幂需要快速求解
b
p
m
o
d
k
b^p\bmod k
bpmodk
一般,我们写递归会写成这个样子:
long long quickpow(long long b,long long p,long long k)
{
if(p==1) return b%k;
if(p==0) return 1%k;
long long ans=quickpow(b,p/2,k);
if(p%2==0) return (ans%k)*(ans%k)%k;
if(p%2==1) return (ans%k)*(ans%k)%k*b%k;
}
还有的会写递推。但是,真正的王者还是位运算:
long long quickpow(long long b,long long p,long long k)
{
long long ans=1%k;
b%=k;
while(p)
{
if(p&1) ans=(ans*b)%k;
p>>=1;
b=(b*b)%k;
}
return ans;
}
4. 集合运算
我们可以将一个自然数集合
S
S
S 压缩成一个二进制数
s
s
s。该二进制数为
s
=
∑
i
∈
S
2
i
s=\sum_{i\in S}2^i
s=i∈S∑2i如果转成了二进制,那么一些集合操作就能够更好地实现。
设有两集合
A
A
A、
B
B
B,已经转成了二进制数
a
a
a、
b
b
b,则
- 空集
⇒
\Rightarrow
⇒
0
- 只含有第
i
i
i 个元素的集合
{
i
}
⇒
\{i\}\Rightarrow
{i}⇒
1<<i
- 含有全部
n
n
n 个元素的集合
{
0
,
1
,
2
,
.
.
.
,
n
−
1
}
⇒
\{0,1,2,...,n-1\}\Rightarrow
{0,1,2,...,n−1}⇒
(1<<n)-1
- 判断第
i
i
i 个元素是否在集合
A
A
A 中
⇒
\Rightarrow
⇒
if(a>>i&1)
- 向集合
A
A
A 中加入第
i
i
i 个元素
⇒
\Rightarrow
⇒
a|1<<i
- 从集合
A
A
A 中取出第
i
i
i 个元素
⇒
\Rightarrow
⇒
a&~(1<<i)
-
A
∩
B
⇒
A\cap B\Rightarrow
A∩B⇒
a&b
-
A
∪
B
⇒
A\cup B\Rightarrow
A∪B⇒
a|b
枚举集合 S S S 的每一个子集时,只需要这样
int sub=s;
do sub=(sub-1)&s;
while(sub!=s);
每个 s u b sub sub 都是 S S S 的子集,且没有缺漏。
5. 状态压缩 DP
状态压缩动态规划,就是我们俗称的状压 DP,是利用计算机二进制的性质来描述状态的一种 DP 方式。
很多棋盘问题都运用到了状压,同时,状压也很经常和 BFS 及 DP 连用。
状压 DP 其实就是将状态压缩成
2
2
2 进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有
1
1
1 或
0
0
0,一类非常典型的动态规划。
6. 补码
二进制负数在计算机中使用补码存储。
假设
a
a
a 是一个二进制正整数,那么
−
a
-a
−a 在计算机中的补码是 ~
a
+
1
a+1
a+1。例如,
−
1
-1
−1 的八位二进制补码是
11111111
11111111
11111111。
知道就好。
7. 其他杂碎
假如有二进制数 x x x,那么
操作 | 运算 |
---|---|
去掉最后一位 | x>>1 |
在最后加一个 0 0 0 | x<<1 |
在最后加一个 1 1 1 | (x<<1)+1 |
最后一位变 1 1 1 | x|1 |
最后一位变 0 0 0 | x|1-1 |
最后一位取反 | x^1 |
右数第 k k k 位变 1 1 1 | x|1<<k-1 |
右数第 k k k 位变 0 0 0 | x&~(1<<k-1) |
右数第 k k k 位取反 | x^1<<k-1 |
末 k k k 位 | x&1<<k-1 |
右数第 k k k 位 | x>>k-1&1 |
末 k k k 位变成 1 1 1 | x|(1<<k-1) |
末 k k k 位取反 | 1^(1<<k-1) |
把右边连续的 1 1 1 变 0 0 0 | x&x+1 |
把右起第一个 0 0 0 变 1 1 1 | x|x+1 |
把右边连续的 0 0 0 变 1 1 1 | x|x-1 |
取右边连续的 1 1 1 | (x^x+1)>>1 |