leetcode(16)二分查找系列题目

基本框架:

704. 二分查找

两种写法

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l, r = 0, n - 1
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                l = mid + 1
            else:
                r = mid - 1
        return -1

或者

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l, r = 0, n    # r 初始化为 n
        while l < r:   # <
            mid = (l + r) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                l = mid + 1
            else:
                r = mid   # 不用- 1
        return -1

35. 搜索插入位置

两种写法
while left <= right:时
return left
return right + 1都可以

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) //2
            if target == nums[mid]:
                return mid
            elif target > nums[mid]:
                left = mid + 1
            else:
                right = mid - 1
        # return left
        return right + 1

或者while left < right:时
return right
return left都可以

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)
        while left < right:
            mid = left + (right - left) //2
            if target == nums[mid]:
                return mid
            elif target > nums[mid]:
                left = mid + 1
            else:
                right = mid
        return right
        # return left

34. 在排序数组中查找元素的第一个和最后一个位置

先找到target的idx,然后左右滑动指针,来找到符合题意的区间

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        def search(nums, target):
            left, right = 0, len(nums) - 1
            while left <= right:
                mid = left + (right - left) // 2
                if target == nums[mid]:
                    return mid
                elif target > nums[mid]:
                    left = mid + 1
                else:
                    right = mid - 1
            return -1
        idx = search(nums, target)
        if idx == -1:
            return [-1, -1]
        left, right = idx, idx
        while left - 1 >= 0 and nums[left - 1] == target:   # 向左滑动,找左边界
            left -= 1
        while right + 1 < len(nums) and nums[right + 1] == target:   # 向右滑动,找右边界
            right += 1
        return [left, right]

69. x 的平方根

return left - 1
return right都可以

class Solution:
    def mySqrt(self, x: int) -> int:
        left, right = 0, x
        while left <= right:
            mid = (left + right) // 2
            if mid * mid == x:
                return mid
            elif mid * mid < x:
                left = mid + 1
            else:
                right = mid - 1
        return right  # 没找到则返回最大的一个平方小于x的即退出循环的right
        # return left - 1

367. 有效的完全平方数

class Solution:
    def isPerfectSquare(self, num: int) -> bool:
        left, right = 0, num
        while left <= right:
            mid = left + (right - left) // 2
            if mid * mid == num:
                return True
            elif mid * mid < num:
                left = mid + 1
            else:
                right = mid - 1
        return False

162. 寻找峰值

通过二分搜索,且选择前或者后对比一个,最终肯定能够找到峰值,因为题目给了一些前提条件:

  • 对于所有有效的 i 都有 nums[i] != nums[i + 1]
    • 也就是说相邻的数字都是不等的
  • 题目条件给出 nums[-1] = nums[n] = -∞
    • 这个条件很关键,当只有一个元素的时候,它自己本身就是峰值
    • 当有多个元素的时候,如果单调递增,那最后一个元素是峰值,因为 nums[n] = -∞
    • 当有多个元素的时候,如果单调递减,那第一个元素是峰值,因为 nums[-1] = -∞
    • 当有多个元素的时候,不是单调区间,而且题目说了相邻元素不相等,一定能够在 [0, n-1] 区间内找到峰值的

二分法找最大值,只能while left < right:
举例子[1,2,1],[2,2,1]

class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        n = len(nums)
        left, right = 0, n - 1
        # 在这里再减去 1,主要是为了避免 nums[mid+1] 数组越界,而且又不妨碍最终的答案落在 len(nums) - 1 上
        # 巧妙至极
        while left < right:
            # mid = (left + right) // 2
            mid = left + (right - left) // 2
            if nums[mid] > nums[mid + 1]:
            # 最后比较的mid和mid + 1其实就是left和right
                right = mid 
                # 移动右边界,最右边就是最大值
            else:
                left = mid + 1
                # 移动左边界,最左边就是最大值
        return left
        

633. 平方数之和

class Solution:
    def judgeSquareSum(self, c: int) -> bool:       
        i = 0
        while i*i <= c:
            i += 1
        left, right = 0, i
        while left <= right:
            sum_ =left*left + right*right
            if sum_ == c:
                return True
            elif sum_ < c:
                left += 1
            else:
                right -= 1
        return False
# 暴力法
class Solution:
    def judgeSquareSum(self, c: int) -> bool:
        nums = set()
        i = 0
        while i*i <= c:
            nums.add(i*i)
            i += 1
        # print(nums)
        for n in nums:
            if (c - n) in nums:
                return True
        
        return False

33. 搜索旋转排序数组

先判断左边有序且target在左边,则收缩右边界

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        if not nums:
            return -1
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = (left + right) // 2
            if target == nums[mid]:
                return mid
            elif nums[left] <= nums[mid]:
                if nums[left] <= target < nums[mid]:
                    right = mid - 1
                else:
                    left = mid + 1
            else:
                if nums[mid] < target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid - 1
        return -1

153. 寻找旋转排序数组中的最小值

2023.06.08腾讯一面

class Solution:
    def findMin(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1
        while left < right:
            mid = (left + right) // 2
            if nums[mid] > nums[right]:
                left = mid + 1
            else:
                right = mid
        return nums[right]

154. 寻找旋转排序数组中的最小值 II

2023.06.08腾讯一面
有相同元素就right -= 1

class Solution:
    def findMin(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1
        while left < right:
            mid = (left + right) // 2
            if nums[mid] > nums[right]:
                left = mid + 1
            elif nums[mid] < nums[right]:
                right = mid
            else:
                right -= 1
        return nums[right]

6096. 咒语和药水的成功对数

先排序,再用二分法
类似于找到success插入potions[j] * spells[i]的位置,但返回的是m - left,即插入位置后面的元素个数

class Solution:
    def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
        n, m = len(spells), len(potions)
        potions.sort()
        res = [0] * n
        for i in range(n):
            left, right = 0, m - 1
            while left <= right:
                mid = left + (right - left) //2
                if potions[mid] * spells[i] >= success:
                    right = mid - 1
                else:
                    left = mid + 1
            res[i] = m - left
        return res

或者用除法先向上取整,再调用二分查找的库函数bisect.bisect_left()
这题不向上取整好像也可以,直接t = success / spells[i]
bisect_left函数是新元素会被放置于它相等的元素的前面,而 bisect_right返回的则是跟它相等的元素之后的位置。

for i in range(n):
    t = (success + spells[i] - 1) // spells[i]
    # t = ceil(success / spells[i])
    # t = success // spells[i] + (success % spells[i] != 0)
    res[i] = m - bisect.bisect_left(potions, t)

bisect.bisect_right(a,x,lo=0,hi=len(a),*,key=None)
在有序数组a中[lo,hi]区间内查找x插入的位置,返回索引值。如果a中有跟x相同的元素,则x插入的位置是右边。
key 指定一个 key function 用于从每个输入元素提取比较关键字的一个参数的。默认值为 None (直接比较元素)。

1235. 规划兼职工作

  • 不选第 i 个工作,那么最大报酬等于前 i-1 个工作的最大报酬(转换成了一个规模更小的子问题);
  • 选第 i 个工作,由于工作时间不能重叠,设 j 是最大的满足 \(\textit{endTime}[j]\le\textit{startTime}[i]\) 的 j,那么最大报酬等于前 j 个工作的最大报酬加上 \(\textit{profit}[i]\)(同样转换成了一个规模更小的子问题);
class Solution:
    def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
        jobs = sorted(zip(endTime, startTime, profit))
        dp = [0] * (len(jobs) + 1)
        for i, (end, start, p) in enumerate(jobs):
            # j = bisect_right(jobs, (st, inf), hi = i)  等价于下面一行
            j = bisect_right(jobs, start, hi = i, key = lambda x:x[0])  
            # 查询在jobs中插入(st, inf)的位置因为要找最后一个所以用无穷大
            # print(j)
            dp[i + 1] = max(dp[i], dp[j] + p)
        return dp[-1]

1751. 最多可以参加的会议数目 II

注意:与1235. 规划兼职工作 的区别就是多一维记录参加会议的个数k
定义 f[i][j]表示参加前 i 个会议中的 j 个,能得到的会议价值的最大和。
分类讨论:

  • 不参加第 i 个会议:f[i][j] = f[i-1][j];
  • 参加第 i 个会议:f[i][j]=f[p][j−1]+value[i],其中 p 是最大的满足 \(\textit{endDay}[p]<\textit{startDay}[i]\) 的 p,不存在时为 -1。
class Solution:
    def maxValue(self, events: List[List[int]], k: int) -> int:
        events.sort(key = lambda x: x[1])
        n = len(events)
        dp = [[0] * (k + 1) for _ in range(n + 1)]
        for i, (start, end, value) in enumerate(events):
            p = bisect_left(events, start, hi = i, key = lambda x: x[1])
            for j in range(1, k + 1):
                dp[i + 1][j] = max(dp[i][j], dp[p][j - 1] + value)
        return dp[-1][-1]

2008. 出租车的最大盈利

定义 f[i]表示行驶到 i 时的最大盈利。
考虑状态转移:

  • 一方面,我们可以不接终点为 i 的乘客,这样有 f[i]=f[i-1];
  • 另一方面,我们可以接所有终点为 i 的乘客中收益最大的,这样有 f[i]=max(f[start]+i−start+tip),二者取最大值。
class Solution:
    def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
        rides.sort(key = lambda x: x[1])
        m = len(rides)
        dp = [0] * (m + 1)
        for i, (start, end, tip) in enumerate(rides):
            j = bisect_right(rides, start, hi = i, key = lambda x:x[1])
            profi = end - start + tip
            # 可省略
            # if j > 0:
            #    profi += dp[j]
            dp[i + 1] = max(dp[i], profi)
        return dp[-1]

410. 分割数组的最大值

首先,为什么可以用二分法:

  • 如果对这个数组,每个数单独成一数组,那么子数组的各自和的最大值,就是所有数中的最大值。
  • 而如果对这个数组不分组,那么子数组的各自和的最大值就是这个数组的和。
  • 这两个值对应的就是 left 和 right,即全分组与不分组的结果。

其次,对于二分的每个mid怎么设置判断条件:

  • 假设给定了mid,那么需要判断,如果以mid作为最大值,能形成几组,然后和给定的k值作对比。显然,如果这个mid越大,要分出来的组数越少。
  • 如果形成的组数比要求的多,说明这个给定的mid太小了,要扩大,而如果形成的组数太少了,说明给定的mid太大了,要缩小。
  • 如果相等,假设给定列表[5,123]和m = 2来找出最大值,假如在二分中选择了124,这样子只能分成两个组,而显然124这个数不是正确答案,正确答案是123,所以相等的时候,查找值mid应该缩小(即找左边界)。
class Solution:
    def splitArray(self, nums: List[int], k: int) -> int:
        def group(cur):
            cnt, tmp = 1, 0  # cnt初值为1
            for n in nums:
                if tmp + n > cur:  # 先加上n
                    cnt += 1
                    tmp = n
                else:
                    tmp += n
            return cnt
        left, right = max(nums), sum(nums)
        while left < right:
            mid = (left + right) // 2
            if group(mid) > k:
                left = mid + 1
            else:
                right = mid
        return left

374. 猜数字大小

# The guess API is already defined for you.
# @param num, your guess
# @return -1 if num is higher than the picked number
#          1 if num is lower than the picked number
#          otherwise return 0
# def guess(num: int) -> int:

class Solution:
    def guessNumber(self, n: int) -> int:
        left, right = 1, n 
        while left < right:
            num = (left + right) // 2
            if guess(num) == 0:
                return num
            elif guess(num) == -1:
                right = num
            else:
                left = num + 1
        return left

6242. 二叉搜索树最近节点查询

根据二叉搜索树的中序遍历一定是有序列表这一性质,先全部存到一个列表st中
然后用bisect_right()查找每个q的插入位置

自己的思路是分4种情况:
1.q在列表中,则cur_min和cur_max 都是q
但是用if q in st:超时了,改成if q == st[index - 1]:就好了
2.q小于列表最小值
3.q大于列表最大值
4.q被两个数夹着的

class Solution:
    def closestNodes(self, root: Optional[TreeNode], queries: List[int]) -> List[List[int]]:
        st = []
        def inorder(root):
            if root:
                inorder(root.left)
                st.append(root.val)
                inorder(root.right)
            return st
        inorder(root)
        # print(st)
        res = []
        for q in queries:
            cur_min, cur_max = -1, -1
            index = bisect_right(st, q)
            # print(index)
            if q == st[index - 1]:
                cur_min = cur_max = st[index - 1]
            elif index == 0:
                cur_max= st[index]
            elif index == len(st):
                cur_min = st[index - 1]
            elif 0 < index < len(st):
                cur_min, cur_max = st[index - 1], st[index]
            res.append((cur_min, cur_max))
        return res

大佬的思路:直接用bisect_right(),bisect_left()分别找最小值最大值的位置

class Solution:
    def closestNodes(self, root: Optional[TreeNode], queries: List[int]) -> List[List[int]]:
        st = []
        def inorder(root):
            if root:
                inorder(root.left)
                st.append(root.val)
                inorder(root.right)
            return st
        inorder(root)
        # print(st)
        res = []
        for q in queries:
            i = bisect_right(st, q)
            cur_min = st[i - 1] if i > 0 else -1
            j = bisect_left(st, q)
            cur_max = st[j] if j < len(st) else -1
            res.append((cur_min, cur_max))
        return res

878. 第 N 个神奇数字

class Solution:
    def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
        left = min(a, b)
        right = n * min(a, b) - 1
        c = lcm(a, b)
        while left <= right:
            mid = (left + right) // 2
            cur = mid // a + mid // b - mid // c 
            if cur >= n:
                right = mid - 1
            else:
                left = mid + 1
        return (right + 1) % (10 ** 9 + 7)

1201. 丑数 III

注意这题结果在 **[1, 2 * 10^9] 的范围内,并且是+ num // labc **

class Solution:
    def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int:
        left = 1
        right = min(n * min(a, b, c), 2 * 10 ** 9)
        lab, lac, lbc, labc = lcm(a,b), lcm(a,c), lcm(b,c), lcm(a,b,c)
        def check(num):
            return num // a + num // b + num // c -\
            num // lab - num // lac - num // lbc + num // labc 
        while left <= right:
            mid = (left + right) // 2
            cur = check(mid)
            if cur >= n:
                right = mid - 1
            else:
                left = mid + 1
        return right + 1
        

参考资料:
bisect 二分查找模块
bisect ---数组平分算法
python中的bisect模块与二分查找详情

posted @ 2022-05-17 22:03  YTT77  阅读(39)  评论(0编辑  收藏  举报