位运算
位运算
基础
位运算基础可以见python基础知识
计算位运算计算时间为O(1)
移位运算
左移:
逻辑右移: 就是不考虑符号位,右移一位,左边补零即可
算术右移: 考虑符号位,右移一位,若符号位为1,就在左边补1;否则,就补0
算术右移也可以进行有符号位的除法,右移n位就等于除2的n次方
🍆n>>1是向下取整,n/2是整数除法,是向0取整
1 | [(i // 2, i >> 1) for i in range(10)] |
位运算小技巧
用异或实现配偶
1
2
3
4
5
6
7
8
9
100, 1
2, 3
4, 5
6, 7
0 ^ 1 = 1, 1 ^ 1 = 0
2 ^ 1 = 3, 3 ^ 1 = 2
...
4 ^ 1 = 5, 5 ^ 1 = 4
...1
2
3
4
5
6
7
8[i ^ 1 for i in range(10)]
Out[3]: [1, 0, 3, 2, 5, 4, 7, 6, 9, 8]
[i ^ 2 for i in range(10)]
Out[4]: [2, 3, 0, 1, 6, 7, 4, 5, 10, 11]
[i ^ 3 for i in range(10)]
Out[5]: [3, 2, 1, 0, 7, 6, 5, 4, 11, 10]
[i ^ 4 for i in range(10)]
Out[6]: [4, 5, 6, 7, 0, 1, 2, 3, 12, 13]
lowbit运算: 求1最低的位数
1
2
3
4
5lowbit = lambda n: (-n) & n
或
lowbit = lambda n: (~n+1 ) & n
lowbit(11110010000) = 10000
判断奇偶
只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((a & 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数
取一个数中指定位
取X的低4位,用 X & 0000 1111 = 00001110 即可得到
对一个数的某些位置1
找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1
使特定位翻转
使特定位翻转,该数对应X要翻转的对应位为1,其余位为零,此数与X对应位异或即可
例:X=10101110,使X低4位翻转,用X ^0000 1111 = 1010 0001即可得到
异或的几条性质:异或其实就是不进位加法
1、交换律 a ^ b = c ===> a ^ c = b , b ^ c = a
2、结合律 即 (a ^ b) ^ c == a ^ (b ^ c)
3、对于任何数x,都有x ^ x = 0,x ^ 0 = x
4、自反性: a ^ b ^ b = a ^ 0 = a
1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间
将所有的数全部异或,得到的结果与1 ^ 2 ^ 3 ^ … ^ 1000的结果进行异或,得到的结果就是重复数
枚举一个无重复字符串所有子集的技巧
用二进制数组表示单词中那些字母出现(1表示),那些没出现(0表示),找出该单词的所有子集。
ace 对应二进制 int origin = 21 (二进制10101),包含的子集(n代表子集,初始 n = origin)有:1
2
3
4
5
6
7
8
9ace 10101
ce 10100
ae 10001
ac 00101
e 10000
c 00100
a 00001
遍历核心代码:n = (n - 1) & origin;// n-1 AND puzzleBit,生成一个puzzleBit的新的子集合
可以自己手动推算一下,是对的,n刚好遍历完所有情况找map中 存在的 字符串str = ace(必含有首字母a)子集的个数
1
2
3
4
5
6
7
8
9
10int first = 1 << (str[0] - 'a');
while (n > 0) { // 遍历origin 的所有字母组合,当n=0时终止遍历
// 按位都是1才为1,否则为0,即n这个组合包含origin 的首字母
// 而且n这个组合在map中有值,即有单词长n这样,值累加给res[i]
if ((n & first) != 0 && map[n] > 0) {
res[i] += map[n];
}
// n-1 AND puzzleBit,生成一个puzzleBit的新的子集合
n = (n - 1) & puzzleBit;
}
常用位运算公式
a >> b & 1 代表检查 a 的第 b 位是否为 1,有两种可能性 0 或者 1
a += 1 << b 代表将 a 的第 b 位设置为 1 (当第 b 位为 0 的时候适用)
状态压缩
定义
二进制状态压缩,是指将一个长度为$m$的bool数组用一个$m$位二进制整数表示并存储的方法
利用下列位运算操作可以实现原bool数组中对应下标元素的存取
操作 | 运算 |
---|---|
取出整数$n$在二进制表示下的第$k$位 | n >> k & 1 |
取出整数$n$在二进制表示下的第$0 \sim k-1$位(后$k$位) | n & ((1 << k) - 1) |
把整数$n$在二进制表示下的第$k$位取反 | n ^ (1 << k) |
对整数$n$在二进制表示下的第$k$位赋值$1$ | n $\vert$ (1 << k) |
对整数$n$在二进制表示下的第$k$位赋值$0$ | n & (~(1 << k)) |
这种方法运算简便,并且节省了程序运行的时间和空间
- 当$m$不太大时,可以直接使用一个整数类型存储
- 当$m$较大时,可以使用若干个整数类型(int数组),也可以直接利用C++ STL为我们提供的bitset实现
案例
a^b
题目
描述
求a的b次方对p取模的值
难度
简单
输入格式
三个整数a,b,p,在同一行用空格隔开
输出格式
输出一个整数,表示$(a^b) \% p$的结果
数据范围
输入样例
1 | 3 2 7 |
输出样例
1 | 2 |
思路
快速幂
1 | 3 ^ 10000000 |
🥕核心: 再看下10000000的二进制表示,把对应为1的位乘起来就可以
10000000的二进制表示为0b100110001001011010000000
,因此只要对应位置的值乘起来就是3 ^ 10000000
🥕根据数学常识,每一个正整数可以唯一表示为若干指数不重复的2的次幕的和
1 | def ck_sum_of_power(in_n: int): |
也就是说,如果b在二进制表示下有$k$位,其中第$i(0<i<k)$位的数字是$c_i$,则
于是
因为$k= \lceil log2(b+1) \rceil $,所以上式乘积项的数量不多于$\lceil log2(b+1) \rceil$个
又因为$a^{2^i} = {a^{(2^{i-1})}}^2$,所以我们很容易通过$k$次递推求出每个乘积项,当$c_i=1$时,把该乘积项累积到答案中
b&1运算可以取出b在二进制表示下的最低位,而b>>1运算可以舍去最低位,在递推的过程中将二者结合,就可以遍历b在二进制表示下的所有数位$c_i$,整个算法的时间复杂度为O($log2(b)$)
求解
1 | def solution(a: int, b: int, p: int) -> int: |
a*b%p
题目
描述
求a乘b次方对p取模的值
难度
简单
输入格式
三个整数a,b,p,在同一行用空格隔开
输出格式
输出一个整数,表示$(a*b) \% p$的结果
数据范围
输入样例
1 | 3 4 5 |
输出样例
1 | 2 |
思路
1 | a * b |
求解
1 | def solution(a: int, b: int, p: int) -> int: |
最短Hamilton路径
哈密顿路径
哈密顿路径也称作哈密顿链,指在一个图中沿边访问每个顶点恰好一次的路径。寻找这样的一个路径是一个典型的NP-完全(NP-complete)问题。后来人们也证明了,找一条哈密顿路的近似比为常数的近似算法也是NP完全的
旅行商问题
旅行商问题 (Traveling Salesman Problem,TSP),又叫货郎担问题,它是图论中一个经典的组合优化问题。
经典的TSP可以描述为:一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市一次并且仅一次之后后,回到出发城市。问他应如何选择在城市之间的行程路线,以使他走过的总路程最短。
从图论的角度来看,该问题其实就是在一个赋权的无向图中,去找一个哈密尔顿回路,并且使得该回路的总权值最小。
容易看出,旅行商问题的可行解是N个顶点的全排列(把[1,2,3,4,…,N]打乱顺序随机排列),其可行解的个数有N!个,也就是路线有N!个。
如果按照穷举法将N!个可行解一一找出来,然后从中找出行程最短的路线。随着顶点数N的增加,会产生组合爆炸。从计算复杂性的角度来看,它是NP-完全问题
拓展
P问题,NP问题,NPC问题,NP-Hard问题
题目
描述
给定一张 $n$ 个点的带权无向图,点从 $0∼n−1$ 标号,求起点 $0$ 到终点 $n−1$ 的最短 Hamilton 路径。
Hamilton 路径的定义是从 $0$ 到 $n−1$ 不重不漏地经过每个点恰好一次。
难度
中等
输入格式
第一行输入整数 $n$
接下来 $n$ 行每行 $n$ 个整数,其中第 $i$ 行第 $j$ 个整数表示点 $i$ 到 $j$ 的距离(记为 $a[i,j]$])
对于任意的 $x,y,z$,数据保证 $a[x,x]=0, a[x,y]=a[y,x]$ 并且 $a[x,y]+a[y,z] \geq a[x,z]$
输出格式
输出一个整数,表示最短 Hamilton 路径的长度
数据范围
输入样例
1 | 5 |
输出样例
1 | 18 |
思路
考虑两个点
- 哪些点被用过
- 目前停在哪个点上
第一维有$n$个点,取值为0或1,表示用或不用
第二维有$n$个点,所以状态矩阵大小为
使用暴力方法,状态数为$2^{20}$
接下来考虑每个状态是如何计算的
$state_k$是$state$去掉$j$之后的集合,$state_k$要包含$k$
关键点
这里可以用二进制的整数表示$state$
比如包含$0,1,4$,则$state=10011$
求解
1 | def solution(n, f, weight): |
强调一下(1<<n)-1的意思:1<<n是将1左移n位也就是10000…(n个0),减去1以后就会是1111…(n个1),理解一下,前一个数是n+1位,后一个数是n位,所以说此时(1<<n)-1也就是所有位都是1