二分的本质:二段性 看到有序,找最大、最小值,LeetCode周赛第三题:首先考虑二分查找

板子题

209. Minimum Size Subarray Sum

  1. class Solution:
  2. def minSubArrayLen(self, target: int, nums: List[int]) -> int:
  3. cur = 0
  4. res = inf
  5. i = 0
  6. for j in range(len(nums)):
  7. cur += nums[j]
  8. if cur < target:
  9. continue
  10. while cur >= target:
  11. res = min(res, j - i + 1)
  12. cur -= nums[i]
  13. i += 1
  14. return res if res != inf else 0
  • 时间复杂度:二分查找 - 图1
  • 空间复杂度:二分查找 - 图2

应用题

744. Find Smallest Letter Greater Than Target

看到有序,首先考虑二分。注意当leftright指针最后落在数组尾部(即index == n)时,取数组首个元素。

  1. class Solution:
  2. def nextGreatestLetter(self, letters: List[str], target: str) -> str:
  3. n = len(letters)
  4. left, right = 0, n
  5. while left < right:
  6. mid = left + right >> 1
  7. if letters[mid] <= target:
  8. left = mid + 1
  9. else:
  10. right = mid
  11. return letters[0] if left == n else letters[left]
  • 时间复杂度:二分查找 - 图3
  • 空间复杂度:二分查找 - 图4

875. Koko Eating Bananas

若在每小时能够吃下二分查找 - 图5根香蕉的前提下,能够在二分查找 - 图6小时内吃完所有香蕉(按pile分配),则所有二分查找 - 图7均是符合题意的进食速度;要求其最小值
二分查找 - 图8中,以二分查找 - 图9为分割线(二段性),其实上是以二分思想求其lower_bound

  1. from math import ceil
  2. class Solution:
  3. def minEatingSpeed(self, piles: List[int], h: int) -> int:
  4. # 以每小时'k'的速度进食,能否在'h'小时内吃完所有香蕉
  5. def check(k):
  6. return sum(ceil(p / k) for p in piles) <= h
  7. left, right = 1, max(piles) # 上界max(piles): 每小时只能选择一堆香蕉
  8. while left < right:
  9. mid = left + right >> 1
  10. if check(mid):
  11. right = mid
  12. else:
  13. left = mid + 1
  14. return left
  • 时间复杂度:二分查找 - 图10,其中二分查找 - 图11是香蕉总堆数,二分查找 - 图12是堆中最大香蕉数量
  • 空间复杂度:二分查找 - 图13

其中,天花板÷也可写为:

  1. def ceil_divide(p, k):
  2. return (p - 1) // k + 1

1011. Capacity To Ship Packages Within D Days

设船只的运载能力为二分查找 - 图14,凡是大于二分查找 - 图15的均能在给定时间二分查找 - 图16内运载完所有货物;反之亦然。(二段性)
二分查找 - 图17,求以二分查找 - 图18为分割点的lower_bound

  • 下界:二分查找 - 图19,同一批货物不能拆分
  • 上界:二分查找 - 图20,即一天运完所有货物

其中check()方法的编写值得思量。

  1. class Solution:
  2. def shipWithinDays(self, weights: List[int], D: int) -> int:
  3. # 当前运载能力为'c',能否在'D'天内按顺序、不拆分地运载完所有货物
  4. def check(c):
  5. days = 1
  6. cur = 0
  7. for weight in weights:
  8. if cur + weight > c:
  9. days += 1
  10. cur = 0
  11. cur += weight
  12. return days <= D
  13. left, right = max(weights), sum(weights)
  14. while left < right:
  15. mid = left + right >> 1
  16. if check(mid):
  17. right = mid
  18. else:
  19. left = mid + 1
  20. return left
  • 时间复杂度:二分查找 - 图21,其中二分查找 - 图22是货物总数,二分查找 - 图23是货物的质量之和
  • 空间复杂度:二分查找 - 图24

5219. Maximum Candies Allocated to K Children

设给每个孩子分配二分查找 - 图25个糖果能够满足要求,那么凡是小于二分查找 - 图26的糖果分配数也能满足要求;反之亦然。二段性
二分查找 - 图27,以二分查找 - 图28为分割点,寻找upper_bound

  1. class Solution:
  2. def maximumCandies(self, candies: List[int], k: int) -> int:
  3. # 每人分'm'个糖果(可拆分,不可合并),能否满足'k'个孩子的需求
  4. def check(m):
  5. num_satisfy = 0
  6. for candy in candies:
  7. num_satisfy += candy // m
  8. if num_satisfy >= k:
  9. return True
  10. return False
  11. left, right = 1, sum(candies) // k
  12. if left > right: # 特判:每人一个也不够分
  13. return 0
  14. while left < right:
  15. mid = left + right + 1 >> 1 # 寻找upper_bound
  16. if check(mid):
  17. left = mid
  18. else:
  19. right = mid - 1
  20. return left
  • 时间复杂度:二分查找 - 图29,其中二分查找 - 图30是糖果总堆数,二分查找 - 图31是糖果的数量之和
  • 空间复杂度:二分查找 - 图32

2187. Minimum Time to Complete Trips

设给定时间二分查找 - 图33,并行的车辆能够完成给定的旅途数目,那么所有大于二分查找 - 图34的时间也满足要求;反之亦然。二段性
二分查找 - 图35,以二分查找 - 图36为分割点,寻找lower_bound

  • 下界:跑得最快的车(单趟耗时最短)完成二分查找 - 图37趟旅途所花费的时间
  • 上界:跑得最快的车(单趟耗时最短)完成total_trips旅途所花费的时间

    1. class Solution:
    2. def minimumTime(self, times: List[int], total_trips: int) -> int:
    3. # 消耗时间't',并行的线路能够完成的旅途是否大于或等于'total_trips'
    4. def check(t):
    5. return sum(t // time for time in times) >= total_trips
    6. left, right = min(times), min(times) * total_trips
    7. while left < right:
    8. mid = left + right >> 1
    9. if check(mid):
    10. right = mid
    11. else:
    12. left = mid + 1
    13. return left
  • 时间复杂度:二分查找 - 图38,其中二分查找 - 图39是车辆数目,二分查找 - 图40是车辆最少完成时间,二分查找 - 图41是总趟数

  • 空间复杂度:二分查找 - 图42

162. Find Peak Element

预处理nums = [-inf] + nums + [-inf]后线性扫描二分查找 - 图43,不符合题设的时间复杂度;考虑二分查找
易证:

  1. 对于任意数组(左右两侧填充二分查找 - 图44),一定存在峰值;
  2. 对于一个满足二分查找 - 图45的位置,二分查找 - 图46右边一定存在峰值;或对于一个满足二分查找 - 图47的位置,二分查找 - 图48左边一定存在峰值。

    最早在 33. 搜索旋转排序数组 中,我们强调,二分的本质是「二段性」而非「单调性」,而经过本题,我们进一步发现「二段性」还能继续细分,不仅仅只有满足 **01 特性**(满足/不满足)的「二段性」可以使用二分,满足 **1? 特性**(一定满足/不一定满足)也可以二分。

    • 上坡方向走,一定能找到山峰;
    • 下坡方向走,可能找到山峰。
class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1

        while left < right:
            mid = left + right >> 1
            if nums[mid] > nums[mid+1]:
                right = mid
            else:
                left = mid + 1

        return left
  • 时间复杂度:二分查找 - 图49
  • 空间复杂度:二分查找 - 图50

4. Median of Two Sorted Arrays

容易想到的直观解法如下,时间复杂度二分查找 - 图51

  1. 将两个有序数组合并为一个有序数组,再寻找中位数。 二分查找 - 图52 空间复杂度二分查找 - 图53
  2. 不需要显示合并,利用双指针 + 数组长度找到中位数位置。 二分查找 - 图54 空间复杂度二分查找 - 图55

考虑使用两个数组的**有序**特性:由于已知数组长度,可将寻找中位数的过程转化为求第二分查找 - 图56大的元素。
使用二分查找的思想,不断将二分查找 - 图57一分为二,从两个数组的头部隐式移除(用offset表征)二分查找 - 图58个元素,直到找到第二分查找 - 图59个元素。注意三种特殊情况的判断。

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:

        def get_Kth_element(k):
            offset_1, offset_2 = 0, 0
            while 1==1:
                if offset_1 == m:
                    return nums2[offset_2 + k - 1]
                if offset_2 == n:
                    return nums1[offset_1 + k - 1]
                if k == 1:
                    return min(nums1[offset_1], nums2[offset_2])

                index_1 = min(offset_1 + k//2 - 1, m - 1)  # 若越界,取末尾元素
                index_2 = min(offset_2 + k//2 - 1, n - 1)
                if nums1[index_1] < nums2[index_2]:
                    k -= index_1 - offset_1 + 1            # k减去该段长度,而非'k//2'
                    offset_1 = index_1 + 1
                else:
                    k -= index_2 - offset_2 + 1
                    offset_2 = index_2 + 1

        m, n = len(nums1), len(nums2)
        k1, k2 = (m + n + 1) // 2, (m + n + 2) // 2        # 统一'奇\偶'写法
        return (get_Kth_element(k1) + get_Kth_element(k2)) / 2
  • 时间复杂度:二分查找 - 图60
  • 空间复杂度:二分查找 - 图61
class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:

        # k => 1-index
        def get_Kth_element(k) -> int:
            i = j = 0
            while 1 != 0:
                if i == m:
                    return nums2[j + k - 1]
                if j == n:
                    return nums1[i + k - 1]
                if k == 1:
                    return min(nums1[i], nums2[j])

                if nums1[i] < nums2[j]:
                    i += 1
                else:
                    j += 1
                k -= 1

        m, n = len(nums1), len(nums2)
        k1, k2 = (m+n+1)//2, (m+n+2)//2
        return (get_Kth_element(k1) + get_Kth_element(k2)) / 2
  • 时间复杂度:二分查找 - 图62
  • 空间复杂度:二分查找 - 图63

540. Single Element in a Sorted Array

class Solution:
    def singleNonDuplicate(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1
        while left < right:
            mid = left + right >> 1
            if mid & 1:  # 奇数下标
                if nums[mid] == nums[mid+1]:
                    right = mid
                else:
                    left = mid + 1
            else:        # 偶数下标
                if nums[mid] == nums[mid+1]:
                    left = mid + 1
                else:
                    right = mid
        return nums[left]
  • 时间复杂度:二分查找 - 图64
  • 空间复杂度:二分查找 - 图65

二分查找 - 图66
二分查找 - 图67

class Solution:
    def singleNonDuplicate(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1
        while left < right:
            mid = left + right >> 1
            if nums[mid] == nums[mid ^ 1]:
                left = mid + 1
            else:
                right = mid
        return nums[left]
  • 时间复杂度:二分查找 - 图68
  • 空间复杂度:二分查找 - 图69