
图:闭区间二分循环结束时的左右指针位置(查找第一个 )
刷题建议:初次刷题可以只刷难度低于 分的题目。难度更高的题目常常结合其他算法(数据结构、图论等),等学会其他算法再来刷本题单。
设 为递增(非递减)数组,长为 。
| 需求 | 写法 | 如果不存在 |
|---|---|---|
| 的第一个元素的下标 | 结果为 | |
| 的第一个元素的下标 | 结果为 | |
| 的最后一个元素的下标 | 结果为 | |
| 的最后一个元素的下标 | 结果为 |
| 需求 | 写法 |
|---|---|
| 的元素个数 | |
| 的元素个数 | |
| 的元素个数 | |
| 的元素个数 |
注意 和 互为补集,二者之和为 。 和 同理。
部分题目需要先排序,然后在有序数组上二分查找。
思维扩展:
“花费一个 的时间,增加了一个条件。” —— 二分答案
题目求什么,就二分什么。
问:如何把二分答案与数组上的二分查找联系起来?
答:假设答案在区间 中,我们相当于在一个虚拟数组 中二分找第一个(或者最后一个)值为 的 。这同样可以用红蓝染色法思考。
问:有些题目,明明 可以是答案,但却不在初始二分区间中。比如闭区间二分初始化 (或者开区间 ),这不会算错吗?
答:不会算错。注意「答案所在区间」和「二分区间」是两个概念。想一想,如果二分的 循环每次更新的都是 ,那么最终答案是什么?正好就是 。一般地,如果一开始就能确定 一定可以满足题目要求,那么 是不需要在二分区间中的。换句话说,二分区间是「尚未确定是否满足题目要求」的数的范围。那些在区间外面的数,都是已确定的满足(不满足)题目要求的数。
问:什么是循环不变量?
答:想一想,对于求最小的题目,开区间二分的写法,为什么最终返回的是 ,而不是别的数?在初始化(循环之前)、循环中、循环结束后,都时时刻刻保证 check(right) == true 和 check(left) == false,这就叫循环不变量。根据循环不变量,循环结束时 ,那么 就是最小的满足要求的数(因为再 就不满足要求了),所以答案是 。
注:部分题目可以优化二分边界,减少二分次数,从而减少代码运行时间。对于初次接触二分答案的同学,无需强求自己写出最优的代码,设定一个比较大的二分上界也是可以的。
开区间二分模板(求最小):
class Solution:
# 计算满足 check(x) == True 的最小整数 x
def binarySearchMin(self, nums: List[int]) -> int:
# 二分猜答案:判断 mid 是否满足题目要求
def check(mid: int) -> bool:
# TODO
left = # 循环不变量:check(left) 恒为 False
right = # 循环不变量:check(right) 恒为 True
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
if check(mid): # 说明 check(>= mid 的数) 均为 True
right = mid # 接下来在 (left, mid) 中二分答案
else: # 说明 check(<= mid 的数) 均为 False
left = mid # 接下来在 (mid, right) 中二分答案
# 循环结束后 left+1 = right
# 此时 check(left) == False 而 check(left+1) == check(right) == True
# 所以 right 就是最小的满足 check 的值
return right思维扩展:
在练习时,请注意「求最小」和「求最大」的二分写法上的区别。
前面的「求最小」和二分查找求「排序数组中某元素的第一个位置」是类似的,按照红蓝染色法,左边是不满足要求的(红色),右边则是满足要求的(蓝色)。
「求最大」的题目则相反,左边是满足要求的(蓝色),右边是不满足要求的(红色)。这会导致二分写法和上面的「求最小」有一些区别。
以开区间二分为例:
check(mid) == true 时更新 right = mid,反之更新 left = mid,最后返回 right。check(mid) == true 时更新 left = mid,反之更新 right = mid,最后返回 left。对于开区间写法,简单来说 check(mid) == true 时更新的是谁,最后就返回谁。相比其他二分写法,开区间写法不需要思考加一减一等细节,推荐使用开区间写二分。
开区间二分模板(求最大):
class Solution:
# 计算满足 check(x) == True 的最大整数 x
def binarySearchMax(self, nums: List[int]) -> int:
# 二分猜答案:判断 mid 是否满足题目要求
def check(mid: int) -> bool:
# TODO
left = # 循环不变量:check(left) 恒为 True
right = # 循环不变量:check(right) 恒为 False
while left + 1 < right:
mid = (left + right) // 2
if check(mid):
left = mid # 注意这里更新的是 left,和上面的模板反过来
else:
right = mid
# 循环结束后 left+1 = right
# 此时 check(left) == True 而 check(left+1) == check(right) == False
# 所以 left 就是最大的满足 check 的值
return left # check 更新的是谁,最终就返回谁二分的不是答案,而是一个和答案有关的值(间接值)。
本质是二分答案求最小。二分的 表示上界。
好比用一个盖子(上界)去压住最大值,看看能否压住( 函数)。
本质是二分答案求最大。二分的 表示下界。
例如数组 ,其中第 小、第 小和第 小的数都是 ,第 小和第 小的数都是 。
注 1:一般规定 从 开始,而不是像数组下标那样从 开始。
注 2:部分题目也可以用堆解决。
二分答案的一个难点是 check 函数怎么写,这会涉及到贪心等技巧,可以练练下面的贪心题单(主要是第一章节)。
欢迎关注 B站@灵茶山艾府
如果你发现有题目可以补充进来,欢迎评论反馈。