基础算法 - 位运算

龟速幂

【例题】64 位整数乘法
a × b   %   p a \times b ~\% ~p a×b % p 的值,其中 1 ≤ a , b , c ≤ 1 0 18 1\le a, b, c \le 10^{18} 1a,b,c1018
分析: 因为 a , b a,b a,b 都可能是 1 0 18 10^{18} 1018 所有如果直接相乘会爆long long,不能直接相乘。
方法一:
可以利用类似快速幂的思想,把整数 b b b 用二进制表示,即
b = ( c k − 1 c k − 2 ⋯ c 0 ) 2 = ∑ i = 0 k − 1 ( c i × 2 i ) ,   c i ∈ { 0 , 1 } b = (c_{k - 1}c_{k - 2}\cdots c_0)_2 = \sum_{i = 0}^{k - 1}(c_{i}\times{2^{i}}), ~c_i \in \{0, 1\} b=(ck1ck2c0)2=i=0k1(ci×2i), ci{0,1}
那么式子 a × b a\times b a×b 就等价于
a × b   m o d   p = ∑ i = 0 k − 1 ( a × c i × 2 i )   m o d   p = { ∑ i = 0 k − 1 ( a × c i × 2 i   m o d   p ) }   m o d   p a \times b ~mod~ p = \sum_{i = 0}^{k - 1}(a \times c_{i }\times{2^{i }}) ~mod~ p = \\ \{\sum_{i = 0}^{k - 1}(a \times c_{i }\times{2^{i }} ~mod~ p)\} ~mod~p a×b mod p=i=0k1(a×ci×2i) mod p={i=0k1(a×ci×2i mod p)} mod p
f ( i ) = a × 2 i f(i) = a \times 2 ^ {i } f(i)=a×2i ,可以看出 f ( i ) f(i) f(i) f ( i − 1 ) f(i - 1) f(i1) 具有以下关系:
f ( i ) = f ( i − 1 ) × 2 f(i) = f(i - 1) \times2 f(i)=f(i1)×2
而等式则会变为:
a × b   m o d   p = { ∑ i = 0 k − 1 ( c i × f ( i )   m o d   p ) }   m o d   p a \times b ~mod~ p =\{\sum_{i = 0}^{k - 1}(c_{i }\times f(i) ~mod~ p)\} ~mod~p a×b mod p={i=0k1(ci×f(i) mod p)} mod p
这时我们就可以对中间结果取模了,从而避免溢出。
计算代码如下:

typedef long long LL;
LL  qmul(LL a, LL b, LL p) { //求 a * b % p 
	LL res = 0;
	while(b) {
		if(b & 1) res = (a % p + res % p ) % p;
		a = (a % p + a % p ) % p;
		b >>= 1;
	}
	return res;
}

时间复杂度为 O ( l o g ( n ) ) O(log(n)) O(log(n))

二进制状态压缩

【例题】最短 Hamilton 路径
给定一张 n ( n ≤ 20 ) n(n \le 20) n(n20) 个点的带权无向图,点从 0 0 0 ~ n − 1 n-1 n1 标点,求起点 0 0 0 到终点 n − 1 n - 1 n1 的最短 H a m i l t o n Hamilton Hamilton 路径。

  • H a m i l t o n Hamilton Hamilton 路径的定义是从 0 0 0 n − 1 n - 1 n1 不重不漏地经过每个点恰好一次。

分析: 首先可以很容易想到一种暴力,枚举 n n n 个点的全排列,计算路径长度的最小值,复杂度为 O ( n ! ) O(n!) O(n!) 。使用二进制状态压缩 DP 可以优化到 O ( n 2 × 2 n ) O(n^2 \times 2 ^ n) O(n2×2n)
DP 一般是定义好状态,我们设 F [ i ] [ j ] : = F[i][j] := F[i][j]:= 图上状态为 i i i时,走到点 j j j 的最短路径长度。图上状态就是被压缩到了整数 i i i 上了,因为 i i i 的二进制是一列 01 01 01 序列,在第 i i i 为上的 01 01 01 值就代表图上标点 i i i 是否走过。

理清楚了状态定义后,就可以尝试推出状态转移方程。很显然 F [ 1 ] [ 0 ] = 0 F[1][0] = 0 F[1][0]=0,即只走过标点为 0 0 0 的点,目前就在点 0 0 0 上,所以最短路径长度为零。而其他未知的状态可以设为无穷大,通常使用0x3f3f3f3f赋值。

对于 F [ i ] [ j ] F[i][j] F[i][j] 可以从语义来知道,
F [ i ] [ j ] = min ⁡ i ′ & ( 1 < < k ) = 1 ( F [ i ′ ] [ k ] + w e i g h t [ k ] [ j ] ) ; i ′ = i   x o r   ( i < < j ) F[i][j] = \min_{i'\& (1<< k) =1 }(F[ i'][k] + weight[k][j]);\\ i'=i ~xor~ (i << j) F[i][j]=i&(1<<k)=1min(F[i][k]+weight[k][j]);i=i xor (i<<j)
其中 i ′ i' i状态就是 i i i 状态在第 j j j 个点未走过的状态,在状态 i ′ i' i上所有走过的点都可以到点 j j j ,所有取它们之中的最小值。
代码如下:

int f[1 << 20][20];
memset(f, 0x3f, sizeof f);
f[1][0] = 0;
for(int i =  1; i < 1 << n; ++i ) {
   for(int j = 0; j < n; ++j) if(i >> j  & 1) 
   	for(int k = 0; k < n; ++k) if((i ^ 1 << j) >> k & 1) 
   		f[i][j] = min(f[i][j], f[i ^ 1 << j][k], weight[k][j]);
}

cout << f[(1 << n) - 1][n - 1] << endl;

【例题】起床困难综合症
有一个整数 x x x x x x 的取值在 [ 0 , m ] [0, m] [0,m] 内,找出一个 x x x 使得 x x x 在经过 n n n 次与数字 t i t_i ti 运算后,结果最大。运算有三种 OR、 XOR、AND。
n ≤ 1 0 5 ,    m , t i ≤ 1 0 9 n \le 10^{5}, ~~ m,t_i\le 10^9 n105,  m,ti109
分析: 位运算最大的特点就是在二进制表示下不进位。因此, x x x 的各个位上的 01 01 01 是独立运算的,对于 x x x 的二进制位我们只需看是 0 0 0 优,还是 1 1 1 优。对于每一次填 1 1 1 ,肯定是尽量从最高位选,而每一次选 1 1 1 ,得满足两个条件才能选:

  1. 在已经构造好的前 k k k 位的数字下加上当前的1 << k必须小于等于 m m m
  2. 1 1 1 的运算结果大于选 0 0 0

成对对换

对于非负整数 n n n

  • n n n 位偶数时, n   x o r   1 n ~xor ~ 1 n xor 1 等于 n + 1 n + 1 n+1;
  • n n n 位奇数时, n   x o r   1 n ~xor ~ 1 n xor 1 等于 n − 1 n - 1 n1;

所有,“0与1”,“2与3”,“4与5”,…就是关于 x o r   1 xor~ 1 xor 1运算构成的成对变化。

lowbit 运算

l o w b i t ( x ) : = lowbit(x) := lowbit(x):= 非负整数 n n n 在二进制表示下“最低位的 1 1 1 及其后边所有的 0 0 0 ”构成的数值。
n > 0 n > 0 n>0 n n n 的第 k k k 位是 1 1 1 ,第 0 0 0 ~ k − 1 k - 1 k1 位都是 0 0 0
为了实现 l o w b i t lowbit lowbit 运算,先把 n n n 取反,那么 n n n 的第 k k k 位就是 0 0 0 ,第 0 0 0 ~ k − 1 k - 1 k1 位就是 1 1 1,而第 k k k 位前的都和原来的 n n n 是相反的,这时我们在加上 1 1 1 那么一进位了后, n n n 的第 k k k 位是 1 1 1 ,第 0 0 0 ~ k − 1 k - 1 k1 位都是 0 0 0,且第 k k k 位前的都和原来的 n n n 是相反的。
把一个数取反加一,也正是补码下,把整数变化符号的操作。所以可以推出:
l o w b i t ( x ) = n & ( ∼ x + 1 ) = n & ( − x ) lowbit(x) = n \& ( \sim x + 1) = n \& ( - x) lowbit(x)=n&(x+1)=n&(x)
l o w b i t lowbit lowbit 运算配合 H a s h Hash Hash 表可以找出整数二进制表示下所有是 1 1 1 的位,时间复杂度是 O ( 1 ) O(1) O(1) 的。我们只需要把 n n n 赋值位 n − l o w b i t ( n ) n - lowbit(n) nlowbit(n) ,直至 n = 0 n = 0 n=0 。这是在每一次获取 l o w b i t ( n ) lowbit(n) lowbit(n) 时就间接的告诉了我们末尾 1 1 1 的位置,使用 H a s h [ 2 k ] = k Hash[2^k] = k Hash[2k]=k 的映射,我们就可以 O ( 1 ) O(1) O(1) 的得出末尾 1 1 1 的位置。
代码如下:

const int N = 1 << 20;
int H[N + 1]for(int i = 0; i <= 20; ++i) H[1 << i] = i;

while (cin >> n) {
    while (n > 0) {
        cout << H[n & -n] << ' ';
        n -= n & -n;
    }
    cout << endl;
}

有一个数学技巧就是 ∀ k ∈ [ 0 , 35 ] , 2 k   m o d   37 \forall k \in [0,35], 2^k~ mod ~37 k[035],2k mod 37 互不相等。这样空间就可以从 2 35 2^{35} 235 缩减到 37 37 37

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值