二分思想及其应用
这一篇随笔适用于对二分有一定了解,希望有更多练习的OI选手或是计算机专业学生。
相信学过OI或是计算机专业的同学一定对此不陌生。二分思想充斥在许多算法与数据结构中。作为许多高级算法的基础,二分在计算机的思想中起到一个十分重要的作用。
二分思想的核心是分而治之。一个似乎不太好解的题,想一想二分,寻找两边的子问题的解,不断下沉,便能缩小范围,简化问题。
想想这些问题你会怎么解?
- 在有序数组中寻找某一个确定值的位置
- 求解数组中逆序对问题
- 寻找函数的零点
- 在所有出现的最大情况中,寻找最小值
二分的常见算法是二分查找和二分答案,接下来我将重点讨论两种二分查找实现:
对于第一个问题,我们有一个变式,数组中的值可重复,需要你找到数组中的数的下标尽量小(如果存在的话)
如数组 a [ 6 ] = { 0 , 1 , 1 , 3 , 4 , 6 } a[6]=\{0,1,1,3,4,6\} a[6]={0,1,1,3,4,6},那么如果我们需要寻找 1 1 1 ,那么我们需要输出结果为 2 2 2 ,原因是出现1的位置最小在第 2 2 2 位(还有一个是第 3 3 3 位)。
给出两种二分的模板
左偏二分
int l = 0, r = n - 1; //两个区间端点
while(l < r) {
int mid = (l + r) / 2;
if(a[mid] < k) l = mid + 1;
else r = mid;
}
右偏二分
int l = 0, r = n - 1;
while(l < r) {
int mid = (l + r) - (l + r) / 2;
if(a[mid] <= k) l = mid;
else r = mid - 1;
}
通过演算(需要自己在草稿纸上进行模拟),我们会发现:
- 两个二分的 l , r l,r l,r 均会在最后相等,得到一个值;(结论一)
- 在左偏二分中,值会偏向更小的位置;右偏二分则相反; (结论二)
- 如果数组中不存在值 k k k ,那么左偏二分将偏向比 k k k 大的最小的值,而右偏二分将偏向比 k k k 小的最大值。 (结论三)
具体分析方法可以参考博主往期codeforces题解。
因此,在不同的题中,我们应该根据情况合理选择两个二分。在某些题中甚至需要同时用上两个二分!
题目示例
题目来源:SUSTech_DSAA_lab2_T6
题意
给出一个
N
∗
N
(
1
≤
N
≤
50000
)
N*N(1\le N\le 50000)
N∗N(1≤N≤50000)的矩阵,满足矩阵中第
i
i
i 行第
j
j
j 列的值为
f
[
i
]
[
j
]
=
i
2
+
j
2
+
12345
∗
i
−
12345
∗
j
+
i
∗
j
f[i][j] = i^2+j^2+12345*i-12345*j+i*j
f[i][j]=i2+j2+12345∗i−12345∗j+i∗j
求第
M
(
1
≤
M
≤
N
∗
N
)
M(1\le M \le N*N)
M(1≤M≤N∗N) 小的值。
分析
相信大多数同学会想到使用自己的聪(chong)明(dong)的脑袋瓜子,觉得这道题是一个找规律题(
的确,在
N
≤
65
N\le65
N≤65 之前,会发现:好像是有排序规律的诶。
11
07
04
02
01
16
12
08
05
03
20
17
13
09
06
23
21
18
14
10
25
24
22
19
15
11\ 07\ 04\ 02\ 01\\ 16\ 12\ 08\ 05\ 03\\ 20\ 17\ 13\ 09\ 06\\ 23\ 21\ 18\ 14\ 10\\ 25\ 24\ 22\ 19\ 15\\
11 07 04 02 0116 12 08 05 0320 17 13 09 0623 21 18 14 1025 24 22 19 15
5*5矩阵示例
但实际上,对于一个这样的毫无规律可言的表达式,在 N N N 较大时就已经不适用了。
对表达式 f [ i ] [ j ] = i 2 + j 2 + 12345 ∗ i − 12345 ∗ j + i ∗ j f[i][j] = i^2+j^2+12345*i-12345*j+i*j f[i][j]=i2+j2+12345∗i−12345∗j+i∗j 稍加分析,我们能够发现:
- f [ i ] [ j ] > f [ i − 1 ] [ j − 1 ] f[i][j]> f[i-1][j-1] f[i][j]>f[i−1][j−1] (无效结论,但经常被拿来使用,这也是我为什么卡了很久的原因)
- 固定 j j j 不变,当 i i i 增大时,求导为 f ′ = 2 i + 12345 + j > 0 f'=2i+12345+j>0 f′=2i+12345+j>0 , f f f 增大;
- 固定 i i i 不变,当 j j j 增大时,求导为 f ′ = 2 j − 12345 + i f'=2j-12345+i f′=2j−12345+i , f ′ f' f′的大小和 i , j i,j i,j 均有关系,但基本可确定为先减后增;
观察结论二,实际上便可得到我们的解法:
- 在矩阵的同一列中,这一列数字是有序递增的,在这一列中我们可以使用二分思想;
- 对于每一列,都进行二分;
- 整合,求解。
很明显,我们想找到第 M M M 小的值,但我们无法知道自己所求解的东西是什么。于是,我们可以反过来:
- 假定一个值 k k k ,这是我们假定的答案;
- 实行上述的1,2,3操作,求得在答案k的情况下,比k小的值的个数为 m m m (对于每一行都是如此);
- 将 m m m 与 M M M 进行比较,如果 m m m 更大则减小 k k k 的值,如果 m m m 更小则增大 k k k 的值,如果相等呢?
这里相信细心的同学已经发现了, 1,2,3步执行的是二分查找的工作,4,5,6步执行的是二分答案的操作。并且在这里涉及到了一个问题:如果有相等的数应该偏左还是偏右?如果 M = m M=m M=m 有应该取左还是取右?
让我们进行一下手动模拟的操作:
- 对于每一列的二分查找,我们希望找到小于等于k的数的个数。如果没有数与 k k k 相等,则 m i d mid mid 应该获取比它更小一点的数的位置。由此分析,应选择右偏二分;
- 对于整个统筹的二分答案,如果 M = m M=m M=m ,则应该考虑更小的 k 值(想一想,为什么?),由此应选择左偏二分;
因此,我非常推荐大家以此题来测试自己对二分的熟练度,它很好的将两种二分结合起来,其总复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)
后话
其实,在具体实现的过程中,大家会发现,矩阵中会存在两个相同的数字,因此,在较大的相邻两位可能答案是相同的。而我们的二分查找实际上会偏向更大的值(好比第233位和第234位都是123456,但我们在假定答案位123456时,得到的 m m m 一定为234),为了防止跳过正确答案,在输入为233时,我们应该偏向右边的234位(在二分答案中,不会出现 m = 233 m=233 m=233 这个答案,因此我们应该找相对更大一点的值234,这也是为什么二分答案选择右偏的原因之一)。