龟速幂
【例题】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}
1≤a,b,c≤1018。
分析: 因为
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=(ck−1ck−2⋯c0)2=i=0∑k−1(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=0∑k−1(a×ci×2i) mod p={i=0∑k−1(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(i−1) 具有以下关系:
f
(
i
)
=
f
(
i
−
1
)
×
2
f(i) = f(i - 1) \times2
f(i)=f(i−1)×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=0∑k−1(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(n≤20) 个点的带权无向图,点从
0
0
0 ~
n
−
1
n-1
n−1 标点,求起点
0
0
0 到终点
n
−
1
n - 1
n−1 的最短
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 n−1 不重不漏地经过每个点恰好一次。
分析: 首先可以很容易想到一种暴力,枚举
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
n≤105, m,ti≤109
分析: 位运算最大的特点就是在二进制表示下不进位。因此,
x
x
x 的各个位上的
01
01
01 是独立运算的,对于
x
x
x 的二进制位我们只需看是
0
0
0 优,还是
1
1
1 优。对于每一次填
1
1
1 ,肯定是尽量从最高位选,而每一次选
1
1
1 ,得满足两个条件才能选:
- 在已经构造好的前
k
k
k 位的数字下加上当前的
1 << k
必须小于等于 m m m; - 选 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 n−1;
所有,“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
k−1 位都是
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
k−1 位就是
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
k−1 位都是
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)
n−lowbit(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∈[0,35],2k mod 37 互不相等。这样空间就可以从 2 35 2^{35} 235 缩减到 37 37 37。