二分搜索

常用算法与数据结构模板系列(三)

Posted by John Mactavish on September 5, 2020

binary search 1 binary search 2

几个值得注意的点分别是:

1)区间为左闭右闭,即 [left, right],循环内部的隐性约束是要搜索的数(如果存在的话)在当前搜索的闭区间内。

2)while 循环的条件是 left < right 而不是 left <= right,这样循环终止时,必有 left == right, 使用任意一个都一样,不易出错。

3)区间长度是偶数时涉及到左右中位数的问题,比如 [0,3] 的中位数有 12 两个。 常见的中位数写法有三种:

左中位数 右中位数
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

在默认 leftright 是数组索引,为自然数时,第一种写法 容易整数溢出,不考虑;第二种写法保证不会溢出;第三种写法使用的是 Java 的无符号右移, 即使加和溢出为负数了,移位后的结果仍然是正确的正中位数,这种写法看起来也比第二种清晰,推荐使用。 但是,在 leftright 可能为负数时,第一种使用的除 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