1. 1. 位运算
    1. 1.1. 作用
    2. 1.2. 彩蛋
    3. 1.3. 初步认识
    4. 1.4. 整数的存储与运算
      1. 1.4.1. 补码
      2. 1.4.2. 反码
      3. 1.4.3. 简单表示
    5. 1.5. 移位运算
    6. 1.6. 应用

位运算

作用

计算机底部实现方式是二进制,熟练运用并掌握位运算可以提高程序运行的时空效率

彩蛋

蓝书前言编号0xFF 为 -1,后记为0x7F 为 127(在有符号8位二进制数的条件下)

初步认识

  1. 与:and,&,∧(可以与集合运算联合起来记忆)
  2. 或:or,|,∨
  3. 非:not,~,¬
  4. 异或:xor,^

mm位二进制数中,最低位通常为第00位,从右往左递加,最高位为m1m-1

整数的存储与运算

下面以32位二进制数,即C++中的int和unsigned int 为例子介绍。

补码

  1. unsigned int
    直接把编码C看成32位二进制数S

  2. int
    最高位为符号位,0表示负数,1表示负数。
    符号位为0的,直接把编码C看成32位二进制数S。
    符号位为1的,定义该编码C按位取反后得到的~C表示的数值为1S-1-S

    可发现,在补码下每个数值有唯一的表示方法 并且任意两个数值做加减法时,都等价在32位补码下做最高位不进位(就是最高位溢出的话直接不管了1+1=0,但倒数第二位的进位要进,类似这种)的二进制加减法运算。溢出时,32位符号整数相当于自动对2322^{32}取模。这也解释了“有符号整数”算数上溢时出现负数的现象。(然而并没看懂,有无dalao帮忙解答一下)

反码

略过,就是比补码少加1

简单表示

用二进制表示一个int要32位,比较繁琐,采用十进制又表现不出补码的每一位,所以常用十六进制表示一个常数。这样只用8个字符。
常用数值:0x3F3F3F3F (2倍不会溢出,每个字节相同,方便赋值)

移位运算

左移:在二进制表示下把数字向左移动,低位补0,高位越界舍弃。

1<<n=2n1 << n = 2^n

n<<1=2×nn << 1 = 2 \times n

算数右移:二进制补码表示下,把数字同时向右移动,高位以符号位填充,低位越界后舍弃。等同于除以2后向下取整。((3)>>1=2(-3)>>1=-2
逻辑右移:二进制补码表示下,把输出同时向右移动,高位以0填充,低位越界后舍弃。
一般的编译器均使用算数右移。

应用

  1. abmodpa^b\mod p

根据数学常识,每一个数都可以唯一表示为若干指数不重复的2的次幂的和(因为任何一个整数都可以转换成二进制),也就是说如果bb在简直表示下有kk位,其中第i0iki(0\leq i\leq k)位的数字为cic_i,那么

b=ck12k1+ck22k2+......+c020b=c_{k-1}2^{k-1}+c_{k-2}2^{k-2}+......+c_02^0

于是

ab=ack12k1ack22k2......ac020a^b=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
17
18
19
20
21
22
#include<bits/stdc++.h>

using namespace std;

int main()
{
int a = 0;
int b = 0;
int p = 0;
int ans = 1;
cin >> a >> b >> p;
for(;b > 0;b >>= 1)
{
if(b & 1)
{
ans = (long long)ans * a % p;
}
a = (long long)a * a % p;
}
cout << ans % p;
return 0;
}

在c++语言中,两个数值执行算术运算是,以参与运算的最高数值类型为基准,与保存结果的数值类型无关。

但是这里有一个问题。c++内置的最高整数类型是64位,如运算abmodpa*b \mod p都在101810^{18}级别的话,需要特殊处理

  1. 运算abmodpa*b \mod p1a,b,p10181\leq a,b,p\leq 10^{18}

方法一:类似快速幂思想

b=ck12k1+ck22k2+......+c020b=c_{k-1}2^{k-1}+c_{k-2}2^{k-2}+......+c_02^0

那么

ab=ck1a2k1+ck2a2k2+......+c0a20a*b=c_{k-1}*a*2^{k-1}+c_{k-2}*a*2^{k-2}+......+c_0*a*2^0

这样做就不会超出64位,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int main()
{
ll a = 0, b = 0, p = 0;
cin >> a >> b >> p;
ll ans = 0;
for(;b > 0;b >>= 1)
{
if(b & 1)
{
ans = (ans + a) % p;
}
a = 2 * a % p;
}
cout << ans % p;
}

方法2:
利用abmodp=abab/ppa*b \mod p=a*b-\lfloor a*b/p\rfloor *p。记c=ab/pc=\lfloor a*b/p\rfloor
如何计算cc呢。用浮点数(long double)计算。当浮点数的精度不足以保存到精确数位时,会舍弃低位。所以得到c._ _ _ _c.\_\ \_\ \_\ \_

(long double的作用是,让a*b可以成功的去计算而不会溢出出错,如果采用unsigned long long或long long根本都不能正确计算,更别提误差了,long double即使有误差,但在64位数*64位数乘法中,最大也就到128位,产生的误差也不会过大,也可以得到正确结果)

下面我们再算出abcpa*b-c*p就行了。因为abcpa*b-c*p实际上是abmodpa*b \mod p,所以abcpp<264a*b-c*p \leq p < 2^{64},所以

abcp=(abcp)mod264=(ab)mod264(cp)mod264a*b-c*p=(a*b-c*p)\mod 2^{64}=(a*b)\mod 2^{64} - (c*p) \mod 2^{64}

mod 2642^{64} 就是unsigned long long的自然溢出,再用long long计算最后的结果就行(防止减出负数)

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<bits/stdc++.h>

using namespace std;

typedef unsigned long long ull;
int main()
{
ull a, b, p;
cin >> a >> b >> p;
a %= p, b %= p;
ull c = (long double)a * b / p;
ull x = a * b, y = c * p;
long long ans = (long long)(x % p) - (long long)(y % p);
if(ans < 0) ans += p;
cout << ans % p;
}

本质:将除法转化成减法

  1. 状态压缩dp

二进制状态压缩,指的是将一个长度为mm的bool数组用一个mm位的二进制表示并存储的方法。下列位运算可以实现对应的操作。

操作 运算
取出整数nn在二进制表示下的第kk (n>>k)&1(n>>k)\&1
取出整数nn在二进制表示下的后kk n&((1<<k)1)n\&((1<<k)-1)
把整数nn在二进制表示下的第kk位取反 n xor (1<<k)n\ xor\ (1<<k)
把整数nn在二进制表示下的第kk位赋值为1 n(1<<k)n\vert (1<<k)
把整数nn在二进制表示下的第kk位赋值为0 n&( (1<<k))n \& (~(1<<k))

此方法可以节省大量空间。但当mm过大时,可选择stl里的bitset。

例题:最短Hamilton路径(Acwing91)
“朴素算法”:枚举nn点的全排列,计算长度取最小值,枚举的复杂度O(n!)O(n!),计算长度O(n)O(n),整体复杂度O(nn!)O(n*n!),使用状压dp可以优化到O(n22n)O(n^2*2^n)

我们用一个nn位二进制数来表示

graph LR A[动态规划] A-->B[状态表示f] A-->C[状态计算]