二分
定义
二分查找,也称折半搜索、对数搜索,是用来在一个有序序列中查找某一元素的算法。
理解
使用二分法的前提条件是有序,如果序列无序或者函数不单调,那么二分也就无从谈起。
当问题的答案具有单调性,就可以通过二分法把求解转换为判定。换句话说,二分法将原本从问题逐步求解出答案的过程,转换为在有序的候选答案中逐步查找的过程,直到判定某一候选答案符合问题条件,即得出问题的答案。
算法过程
通过以下示例说明二分法的算法过程:在一个升序数组中查找一个数。
- 考察当前查找范围内的中间元素,判断中间元素是否符合查找条件;
- 如果中间元素小于所查找的值,那么当前查找范围内所有左半部分的元素只会比中间元素更小,因此待查找的元素不会出现在左半部分,只需到右半部分继续查找,将查找范围缩小为右半部分,继续步骤 1;
- 同理,如果中间元素大于所查找的值,将查找范围缩小为左半部分,继续步骤 1;
- 如果中间元素符合查找条件,则查找成功,当前中间元素即为待查找的元素。
- 如果当前查找范围内已经不包含任何元素,则查找失败,原数组中不存在待查找的元素。
代码模板
在代码层面上,我们给出两种二分写法,分别用以处理不同的情况:
- 在单调递增序列 中查找 的数中最小的一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15vector<int> a; // 单调递增序列 a
int l = 0, r = a.size(); // 查找区间为 [l, r)
while(l < r){
int mid = l + ((r - l) >> 1); // 防止 l + r 溢出
if(a[mid] < x){ // 判定中间元素是否满足查找条件
l = mid + 1; // 中间元素不符合条件,需要继续查找右半区间 [mid + 1, r) 是否有更优解
}
else{
r = mid; // 中间元素符合条件,需要继续查找左半区间 [l, mid) 是否有更优解
}
}
if(l == a.size()) ... // 序列 a 中不存在待查找的值
else ... // a[l] 即为待查找的值 - 在单调递增序列 中查找 的数中最大的一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15vector<int> a; // 单调递增序列 a
int l = -1, r = a.size() - 1; // 查找区间为 (l, r]
while(l < r){
int mid = l + ((r - l + 1) >> 1); // 防止 l + r 溢出,r - l + 1 是防止 r - l = 1 时出现死循环
if(a[mid] <= x){ // 判定中间元素是否满足查找条件
l = mid; // 中间元素符合条件,需要继续查找右半区间 (mid, r] 是否有更优解
}
else{
r = mid - 1; // 中间元素不符合条件,需要继续查找左半区间 (l, mid - 1] 是否有更优解
}
}
if(r == -1) ... // 序列 a 中不存在待查找的值
else ... // a[l] 即为待查找的值
如何选择二分写法,关键在于通过分析具体问题,确定左右半部分哪一个是可行区间,以及中间元素应该归属于哪半部分。
最终,如果 ,则查找成功, 即为答案所处的位置;如果 ,则查找失败,原序列中不存在符合条件的元素。
具体应用
整数集合上的二分
在单调序列 中(以非递减序列为例)查找数值 。
采用二分写法 1,即可查找到序列 中数值 第一次出现的位置。
采用二分写法 2,即可查找到序列 中数值 最后一次出现的位置。
实数集合上的二分
在实数区间 上查找满足条件的数值 。
1 | |
1 | |
一元三次方程求解
有形如: 这样的一个一元三次方程。给出该方程中各项的系数( 均为实数),并约定该方程存在三个不同实根(根的范围在 至 之间),且根与根之差的绝对值 。要求由小到大依次输出这三个实根(一个根占一行),并精确到小数点后 2 位。
令 ,即题目是求 的解,且三个答案都在 至 之间。
由于两个根的差的绝对值 ,保证了每一个大小为 1 的区间里至多有 1 个解,也就是说当大小为 1 的区间的两个端点的函数值异号时区间内一定有且只有一个解,同号时一定没有解。
那么可以枚举互相不重叠的每一个长度为 1 的区间,如果区间内有解,可以在区间内进行二分查找。
1 | |
二分答案
一般而言,宏观的最优化问题可以抽象为函数,其“定义域”是这一问题下的解决方案,每个方案对应一个评估值来反映该方案的优劣程度,所有评估值构成函数的“值域”,最优解即为评估值最优的方案。
假设最优解的评估值为 ,且评估值越高越优,则显然这一问题的值域存在「二段性」—— 当评估值小于等于 时,一定存在一个该问题的可行方案;而当评估值大于 时,一定不存在该问题的可行方案。
因此,可以通过二分法,将求最优解的问题,转换为在单调递增区间内(即函数值域)查找使得问题存在可行方案的最大评估值。在划分左右区间时,以问题是否存在可行方案作为条件,如果当前中间值 ,判定存在一个可行方案评分达到 ,则最优评估值一定大于 ,即在右半部分;反之,则右半部分的评估值一定都不存在一个可行方案,则最优评估值一定小于 ,即在左半部分。
这一类问题最经典的就是“最小值最大化”和“最大值最小化”。
以“最小值最大化”为例,要求满足某种条件的最小值的最大可能情况(最小值最大化)。
首先的想法是从大到小枚举这个作为答案的最小值,然后去判断这个值在问题条件的限制下是否存在合法方案,使得这个值在这个方案下是最小值。一旦枚举到某个值,使得问题存在合法方案,则这个值就是最大的“最小值”。若答案区间单调,就可以使用二分法来更快地找到答案。
这里的评估值就是这个“最小值”,要求这个数值越大越优。而问题存在可行方案,就是这个数值在这个可行方案中是最小值。
“最大值最小化”同理。
砍树
伐木工人需要砍 米长的木材。现在有一台伐木机,工作流程如下:设置一个高度参数 (米),伐木机升起一个巨大的锯片到高度 ,并锯掉所有树比 高的部分(当然,树木不高于 米的部分保持不变)。被锯下的部分可以作为木材。伐木工人非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助工人找到伐木机锯片的最大整数高度 ,使得他能得到的木材至少为 米。换句话说,如果再升高 米,他将得不到 米木材。
问题可以转化为,在能够得到 米木材的条件下,求可以设置的最大锯片高度 。
这一问题具有「二段性」。当锯片高度小于等于 时,一定能够得到大于等于 米的木材;而当锯片高度大于 时,一定不能得到大于等于 米的木材。
因此可以考虑以能否得到 米木材作为条件二分锯片高度。
1 | |
三分
对于单峰函数,通常使用二分法衍生出的三分法求单峰函数的极值点。
单峰函数,是一类函数的总称,拥有唯一的极大值点,在极大值点左侧函数严格单调递增,右侧函数严格单调递减;或者拥有唯一的极小值点,在极小值点左侧函数严格单调递减,右侧函数严格单调递增。为避免混淆,也称后一种为单谷函数。
以求解单峰函数 的极大值为例,函数定义域为 。
三分法与二分法的基本思想类似,但每次操作需在当前区间 内任取两点 和 (),把函数分为三部分。通过比较 和 的大小关系,确定极值的位置。
- 如果 ,则 和 要么同时处于极大值点左侧(单调递增函数段),要么处于极大值点两侧。无论哪种情况下,极大值点都在 右侧,则可以舍弃 左侧区间(令 )。
- 同理,如果 ,极大值点都在 左侧,则可以舍弃 右侧区间(令 )。
- 如果 ,则 和 必定处于极大值点的两侧,此时既可以舍弃 左侧区间,也可以舍弃 右侧区间,二者取其一即可。
在实际选择三分点 和 时,如果取 和 为当前区间的三等分点,则区间范围每次缩小 。为减少三分法的操作次数,应使每次舍弃的区间尽可能大。因此,在具体实现时,每一次操作的 和 分别取 和 ( 为二等分点),这能够保证区间范围每次缩小接近 ,从而达到与二分法相近的时间复杂度。
1 | |