几个值得注意的点分别是:
1)区间为左闭右闭,即 [left, right]
,循环内部的隐性约束是要搜索的数(如果存在的话)在当前搜索的闭区间内。
2)while 循环的条件是 left < right
而不是 left <= right
,这样循环终止时,必有 left == right
,
使用任意一个都一样,不易出错。
3)区间长度是偶数时涉及到左右中位数的问题,比如 [0,3]
的中位数有 1
和 2
两个。
常见的中位数写法有三种:
左中位数 | 右中位数 |
---|---|
mid = (left + right) / 2 | mid = (left + right+1) / 2 |
mid = left + (right - left) / 2 | mid = left + (right - left + 1) / 2 |
(left + right) >>> 1 |
(left + right + 1) >>> 1 |
在默认 left
和 right
是数组索引,为自然数时,第一种写法
容易整数溢出,不考虑;第二种写法保证不会溢出;第三种写法使用的是 Java 的无符号右移,
即使加和溢出为负数了,移位后的结果仍然是正确的正中位数,这种写法看起来也比第二种清晰,推荐使用。
但是,在 left
和 right
可能为负数时,第一种使用的除 2 取商是向 0 取整的而不是向下取整的;
第三种写法会把负数移位成正数,直接就不对了;但第二种写法始终是正确的。
4)二分搜索一旦没写好很容易在区间只有两个数时进入死循环,所以判断中位数后一个边界应该在满足 1) 中的约束
的同时更加激进地缩小区间(left = mid + 1
或者 right = mid - 1
)。同时中位数应该选择更加激进的那一边的,
即 left = mid + 1
对应左中位数,right = mid - 1
对应右中位数。
5)至于书写思路,建议先写分支逻辑,并且先写排除中位数的逻辑分支(因为更多时候排除中位数的 if
条件
容易想,不过这并不绝对),另一个分支的逻辑你就不用想了,else
就是它的取反。再根据分支的情况
选择使用左中位数还是右中位数。
可以练习这些题目:
二分搜索的问题不一定会问得那么明显,也可能是这样的:
410. 分割数组的最大值
给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。
设计一个算法使得这 m 个子数组各自和的最大值最小。
示例 :
输入:nums = [7,2,5,10,8], m = 2
输出:18
解释:
一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
1760. 袋子里最少数目的球
给你一个整数数组 nums ,其中 nums[i] 表示第 i 个袋子里球的数目。同时给你一个整数 maxOperations 。
你可以进行如下操作至多 maxOperations 次:
选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。
你的开销是单个袋子里球数目的 最大值 ,你想要 最小化 开销。
请你返回进行上述操作后的最小开销。
示例 :
输入:nums = [9], maxOperations = 2
输出:3
解释:
- 将装有 9 个球的袋子分成装有 6 个和 3 个球的袋子。[9] -> [6,3] 。
- 将装有 6 个球的袋子分成装有 3 个和 3 个球的袋子。[6,3] -> [3,3,3] 。
装有最多球的袋子里装有 3 个球,所以开销为 3 并返回 3 。
这种要求最大化最小值
、最小化最大值
的问题一般都可以用二分搜索解决。
出发点是操作方案难以用代码表示,比如在 1760. 袋子里最少数目的球 中,你要怎么建模?暴力搜索并在搜索树的
每个结点用一个列表表示所有袋子?不现实。而且不难发现达到最优解的方案远不止一种。但是这些问题
正向困难、反向容易。给定一个最值检查它满不满足要求,简单吗?简单。那我们不停猜这个最值
不就好了吗。猜测过程用二分搜索加速。
例如:
# 1760. 袋子里最少数目的球
class Solution:
def minimumSize(self, nums: List[int], maxOperations: int) -> int:
def check(limit: int) -> bool:
operations = 0
for num in nums:
operations += num // limit
if num % limit == 0:
operations -= 1
return operations <= maxOperations
left, right = 1, max(nums)
while left < right:
mid = (left + right) >> 1
if not check(mid):
left = mid + 1
else:
right = mid
return right
参考:
最后附上 GitHub:https://github.com/gonearewe