手把手教你学会二分算法

手把手教你学会二分算法
yngcyUm_nik: Stop learning useless algorithms, go and solve some problems, learn how to use binary search!
1、基本概述
二分算法的基本用法是在一个单调有序的集合或函数中查找一个解。因此,当问题的答案具有单调性时,可以考虑将问题的解,通过二分,转化为某种判定问题。每次分成左右两个区间,通过判断调整上下界,即继续搜索左区间还是右区间,直到找到目标元素。
更进一步的说,还可以扩展到三分算法取解决峰值函数的极值问题。三分算法将区间分成 3 个部分,可以明确判断答案不在哪一部分,舍弃三分之一的空间,继续进行查找。
此外,还有浮点数上的二分,其与整数二分大同小异。
总结一下,一般初学者在练习时可能会碰到以下问题:
- 整数集上的二分,需要注意左右区间的开闭情况,避免漏掉答案或者死循环;
- 实数域上的二分,需要注意精度问题。
二分的实现方法灵活多样,主要是处理边界上的差异时需要仔细考虑。为了避免混淆写法,下面只着重介绍一种实现方法。
2、算法实现
2.1、整数集上的二分
本文介绍的写法保证答案在
举个例子,比如我们要查找区间中最小的大于等于
1 | while (l < r) { |
首先,若
若
相信上面的解释已经让你有些了解二分的基本思路。下面再举一个例子,在不降序列中查找小于等于
1 | while (l < r) { |
尝试按照上面的步骤,自己解释一下上述代码。
可见这个二分写法有两种形式:
,对于中间值,有 ; ,对于中间值,有 。
为什么要区分
主要是因为要避免死循环。假如让形式2为
这种写法比较灵活,其好处是循环退出的条件是
为了进一步加深理解,我们观察这两种形式,不难发现:主要是查找到两个相邻的两个值时,先去判断左端点还是右端点,前者先判断的是左端点,后者先判断的是右端点。所以我们可以利用这个特点,来决定具体采用那哪种形式。
例如,如果我们要找的是最大的一个解,那么在二分查找的过程中,区间会不断缩小,直至两个端点相邻。此时,先去判断较大的那个端点
此外,我们也可以利用这一特点,发现:形式1不会取到
总结一下吧,这种写法的基本流程是:
- 通过分析问题,确定对于
mid
,下一次要查找的是左半段还是右半段,以及mid
保留在哪一个半段; - 根据分析结果,确定
mid
是用哪种形式,相邻时先判断右端点还是左端点; - 循环退出的条件是
,所以最后答案任意输出其中一个即可。
这里提一下 C++ STL 中的两个函数 upper_bound
和 lower_bound
,分别是查找第一个大于
这里总结了以下情况:
- 查找第一个大于
的元素的位置: upper_bound()
。例如,pos = upper_bound(a, a + n, x) - a
; - 查找第一个大于等于
的元素的位置: lower_bound()
; - 查找第一个与
相等的元素: lower_bound()
且; - 查找最后一个与
相等的元素: upper_bound()
的前一个且; - 查找最后一个小于等于
的元素: upper_bound()
的前一个; - 查找最后一个小于
的元素: lower_bound()
的前一个。
2.2、实数域上的二分
也叫浮点二分,与整数集上的二分的写法大同小异,主要考虑精度问题。一般题目会给出保留小数位数或误差范围 EPS
常量为 1e-12
(具体根据题目设置),循环退出的条件由原来的
1 | while (l + eps < r) { |
此外,还有种固定循环次数的写法,不过这种写法一般不太会用到,主要是针对有些题目精度没有明确给出时使用:
1 | for (int i = 0; i < 100; i++) { |
2.3、三分求凸性函数极值
凸性函数即在区间内是单峰函数,有唯一的极大值或极小值点(只能选其一)。
实现的思路是:对于一个开口向下的函数
- 如果
,则 和 同时在极大值的左侧,或是一个在左一个在右。无论是哪种情况, 一定是在极大值的左侧,所以让 ; - 如果
,则 和 同时在极大值的右侧,或是一个在左一个在右。无论是哪种情况, 一定是在极大值的右侧,所以让 ; - 通过上述的两个过程,区间不断往极大值逼近,最终求的解。
1 | while (l + eps < r) { |
3、例题讲解
3.1、整数集上的二分
3.1.1、寻找指定和的整数对
问题描述
输入
题解
采用暴力的做法是用嵌套循环,枚举出所有的情况,时间复杂度为
采用二分算法优化查找。首先要确保序列的单调性,所以对数组进行排序。在查找的过程中,假设当前的数字是 sort
函数,所以最后整个算法的复杂度是
练习:P1102 A-B 数对 - 洛谷(luogu.com.cn)
3.2、实数集上的二分
3.2.1、小车问题
问题描述
两人一车,人比车快,求它们从 A 到 B 的最短时间。
题解
假设甲、乙两人甲先坐车,乙走路。
不难想到最短的时间是两人同时出发,车先带甲到半路,然后让甲自己走到终点,车子再回去接乙,最终两人同时到达终点。此时花费的时间就是最少的时间。所以问题的关键是找到这个车子折返点。
简单分析一下,如果折返点距离起点越远,那么乙花费的时间就越长;如果折返点距离起点越近,那么甲花费的时间就越长。因此可以发现距离
具体做法是枚举
练习:P1258 小车问题 - 洛谷 (luogu.com.cn)
3.3、二分答案
3.3.1、Balanced Stone Heaps
问题描述
有
- 从第
堆石头开始操作,直到第 堆石头; - 假设当前是第
堆石头,可以选择 个石头移到第 堆,同时 个石头移到第 堆,所以如果这样操作之后,第 堆减少了 个石头。 - 每次操作
满足 。
求操作之后最少石头的那堆的最大值。
题解
对最小高度二分。对于当前假设的高度
练习:Problem - 1623C - Codeforces
3.4、三分
3.4.1、Error Curves
问题描述
定义
题解
二次函数在定义域内找出极值用三分即可。然后再取最大值。
练习:Problem - 3714 (hdu.edu.cn)
4、习题推荐
- Problem - 1443C - Codeforces
- Problem - 1408C - Codeforces
- Problem - 626C - Codeforces
- Problem - 371C - Codeforces
- Problem - 2899 (hdu.edu.cn)
- Problem - 4004 (hdu.edu.cn)
- Problem - 2289 (hdu.edu.cn)
- Problem - A - Codeforces
- Problem - 1486D - Codeforces
- Problem - 1119D - Codeforces
- Problem - 1199C - Codeforces
- Problem - 1041D - Codeforces
- Problem - 1370D - Codeforces
- Problem - 1260D - Codeforces