leetcode分类刷题:二分查找(Binary Search)(四、基于值域的数组/矩阵类型)

基于值域的二分法与基于定义域的题型不同,它的目标是从一“特殊排序序列”中确定“第k个元素值”,而不像基于定义域的题型是从排序序列中找小于等于特定target值的第一个索引;同时,针对“特殊排序序列”,往往需要嵌套使用双指针法进行操作,进一步增加了对应题型的难度。

378. 有序矩阵中第 K 小的元素

from typing import List
'''
378. 有序矩阵中第 K 小的元素
题目描述:给你一个n x n矩阵matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一个内存复杂度优于O(n2) 的解决方案。
示例 1:
    输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
    输出:13
    解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13
题眼:每行和每列元素均按升序排序 有序矩阵
思路:(有点难度,看了官方解答才明白)值域的二分法:确定左右两侧边界为matrix[0][0]=leftVal和matrix[n-1][n-1]=rightVal,二分后统计小于等于midVal的数量,
      讨论与k的大小关系,保证第k小的数始终位于leftVal~rightVal之间(即保证在左闭右闭区间内[leftVal,rightVal]),当leftVal==rightVal时,第k小的数即被找出
      这道题就是在“二分式”缩小目标值的值域区间范围!也可以看到,这个题的target值不像之前一样是给定然后确定存在位置了,是要确定其值。
'''


class Solution:
    def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
        n = len(matrix)
        leftVal, rightVal = matrix[0][0], matrix[n-1][n-1]
        # 确定target所在的区间[leftVal, rightVal],对矩阵中小于等于中间值midVal的元素数目进行统计
        # 关键思路:如果数目小于k,说明target>midVal;即左边界leftVal = midVal + 1
        # 如果数目等于k,说明target<=midVal;即右边界rightVal = midVal
        # 如果数目大于k,说明target<=midVal;即右边界rightVal = midVal
        while leftVal < rightVal:
            midVal = leftVal + (rightVal-leftVal) // 2
            count = self.checkSmallEqualMid(matrix, midVal)  # 表示小于等于mid的数量
            if count < k:  # 第k小的数在midVal的右侧,且不包含midVal
                leftVal = midVal + 1
            elif count >= k:  # 第k小的数在midVal的左侧,可能包含midVal
                rightVal = midVal
        return leftVal

    def checkSmallEqualMid(self, matrix: List[List[int]], midVal: int) -> int:
        n = len(matrix)
        # 双指针:根据矩阵的特殊性质,从左下角开始统计
        i, j = n - 1, 0
        count = 0
        while i >= 0 and j <= n - 1:  # 索引必须在边界之内
            if matrix[i][j] <= midVal:
                count += i + 1  # 对应行及行以上全部统计
                j += 1  # 并更新列数
            else:
                i -= 1  # 更新行数
        return count


if __name__ == '__main__':
    obj = Solution()
    while True:
        try:
            in_line = input().strip().split('=')
            matrix = []
            for row in in_line[1].strip()[1: -4].split(']')[:-1]:
                matrix.append([int(n) for n in row.split('[')[1].split(',')])
            k = int(in_line[2])
            # print(matrix, k)
            print(obj.kthSmallest(matrix, k))
        except EOFError:
            break

373. 查找和最小的 K 对数字

from typing import List
'''
373. 查找和最小的 K 对数字
题目描述:给定两个以 升序排列 的整数数组 nums1 和 nums2,以及一个整数 k。
定义一对值(u,v),其中第一个元素来自nums1,第二个元素来自 nums2。
请找到和最小的 k个数对(u1,v1), (u2,v2) ... (uk,vk)。
示例 1:
    输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
    输出: [1,2],[1,4],[1,6]
    解释: 返回序列中的前 3 对数:
        [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
题眼:组合num1为行索引,num2为列索引,可实现 每行和每列元素均按升序排序 有序矩阵,类似于”378. 有序矩阵中第 K 小的元素“
思路:值域二分法,注意返回的不是第k小,而是前k小(看了官网解析也觉得很难,这个题应该更适合其它算法来解)
'''


class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        result = []
        m, n = len(nums1), len(nums2)
        # 第一步,值域二分法找到第k小的数,确定pairSum所在的区间[leftVal, rightVal],对矩阵中小于等于中间值midVal的元素数目进行统计
        leftVal, rightVal = nums1[0]+nums2[0], nums1[m-1]+nums2[n-1]
        while leftVal < rightVal:
            midVal = (leftVal + rightVal) // 2
            count = self.checkSmallEqualMid(nums1, nums2, midVal)  # 表示小于等于中间值midVal的数量
            if count < k:  # 第k小的数在midVal的右侧,且不包含midVal
                leftVal = midVal + 1
            elif count >= k:  # 第k小的数在midVal的左侧,可能包含midVal
                rightVal = midVal
        pairSum = leftVal

        # 第二步,将小于pairSum的数对升序添加到result中
        # 分为两步添加是为了避免等于pairSum的数对和太多,导致所有小于的没有被添加到result中
        # 双指针:根据两个数组的特殊性质,i取nums1的最小值位置,j取nums2的最大值位置(以nums1为基准进行)
        i, j = 0, len(nums2) - 1
        while i < len(nums1) and j >= 0:  # 索引必须在边界之内
            if nums1[i] + nums2[j] < pairSum:
                for t in range(j + 1):
                    result.append([nums1[i], nums2[t]])
                    # if len(result) == k:  # 这里可以不用判断,小于pairSum的数对一定小于k个
                    #     return result
                i += 1
            elif nums1[i] + nums2[j] >= pairSum:
                j -= 1
        # 以下过程是上述双指针法 的等价形式
        # i2 = n - 1
        # for i1 in range(m):
        #     while i2 >= 0 and nums1[i1] + nums2[i2] >= pairSum:  # 用while直到小于pairSum的数对出现
        #         i2 -= 1
        #     for j in range(i2 + 1):  # 将该索引之前的nums2构成的数对,包括本身,全部添加到result
        #         result.append([nums1[i1], nums2[j]])
        #         if len(result) == k:
        #             return result

        # 第三步,将等于pairSum的数对升序添加到result中
        # 双指针:根据两个数组的特殊性质,i取nums1的最小值位置,j取nums2的最大值位置(以nums1为基准进行)
        i, j = 0, len(nums2) - 1
        while i < len(nums1) and j >= 0:  # 索引必须在边界之内
            if nums1[i] + nums2[j] < pairSum:
                i += 1
            elif nums1[i] + nums2[j] > pairSum:
                j -= 1
            else:
                t = j  # 添加nums2元素时考虑重复情况,同时j的位置要被记录而不能被更新
                while t >= 0 and nums1[i] + nums2[t] == pairSum:
                    result.append([nums1[i], nums2[t]])
                    if len(result) == k:
                        return result
                    t -= 1
                i += 1
        # 以下过程是上述双指针法 的等价形式
        # i2 = n - 1
        # for i1 in range(m):
        #     while i2 >= 0 and nums1[i1] + nums2[i2] > pairSum:  # 用while过滤掉大于pairSum的数对
        #         i2 -= 1
        #     j = i2
        #     while j >= 0 and nums1[j] + nums2[i2] == pairSum:  # 用while是为了将nums2中的全部重复元素添加上
        #         result.append([nums1[i1], nums2[j]])
        #         if len(result) == k:
        #             return result
        #         j -= 1
        return result

    def checkSmallEqualMid(self, nums1: List[int], nums2: List[int], midVal: int) -> int:
        # 双指针:根据两个数组的特殊性质,i取nums1的最小值位置,j取nums2的最大值位置
        i, j = 0, len(nums2) - 1
        count = 0
        while i < len(nums1) and j >= 0:  # 索引必须在边界之内
            if nums1[i] + nums2[j] <= midVal:
                count += j + 1  # 对应nums2位置元素及之前的全部统计
                i += 1
            else:
                j -= 1
        return count


if __name__ == '__main__':
    obj = Solution()
    while True:
        try:
            in_line = input().strip().split('=')
            nums1 = [int(n) for n in in_line[1].strip().split('[')[1].split(']')[0].split(',')]
            nums2 = [int(n) for n in in_line[2].strip().split('[')[1].split(']')[0].split(',')]
            k = int(in_line[3])
            # print(nums1, nums2, k)
            print(obj.kSmallestPairs(nums1, nums2, k))
        except EOFError:
            break

719. 找出第 K 小的数对距离

from typing import List
'''
719. 找出第 K 小的数对距离
题目描述:数对 (a,b) 由整数 a 和 b 组成,其数对距离定义为 a 和 b 的绝对差值。
给你一个整数数组 nums 和一个整数 k ,数对由 nums[i] 和 nums[j] 组成且满足 0 <= i < j < nums.length 。
返回 所有数对距离中 第 k 小的数对距离。
示例 1:
    输入:nums = [1,3,1], k = 1
    输出:0
    解释:数对和对应的距离如下:
    (1,3) -> 2
    (1,1) -> 0
    (3,1) -> 2
    距离第 1 小的数对是 (1,1) ,距离为 0 。
题眼:第k小,必然是要经过排序的
思路:排序+值域二分法(完全想不到和“378. 有序矩阵中第 K 小的元素”、“373. 查找和最小的 K 对数字”是类似题型:
注意最小取值为0,最大为末端减去首端)+双指针
'''


class Solution:
    def smallestDistancePair(self, nums: List[int], k: int) -> int:
        # 第一步,将数组升序排列
        nums.sort()
        n = len(nums) - 1
        # 第二步,值域二分法:确保第k小的 数对距离,确定所在的区间[leftVal, rightVal]
        leftVal, rightVal = 0, nums[n] - nums[0]  # 注意最小取值为0,最大为末端减去首端
        while leftVal < rightVal:
            midVal = (leftVal + rightVal) // 2
            count = self.countSmallEqual(nums, midVal)  # 表示小于等于中间值midVal的数量
            if count < k:  # 第k个数对距离 一定大于midVal
                leftVal = midVal + 1
            elif count >= k:  # 第k个数对距离 可能小于或等于midVal
                rightVal = midVal
        return leftVal

    def countSmallEqual(self, nums: List[int], midVal: int) -> int:
        # 双指针统计小于等于midVal的数对距离个数
        count = 0
        i, j = 0, 1  # i,j指向数组第一、第二位置
        while i < len(nums) - 1:  # i
            if j < len(nums) and nums[j] - nums[i] <= midVal:
                j += 1
            elif j < len(nums) and nums[j] - nums[i] > midVal:  # 满足统计条件:索引全部有效时,第一次出现超过midVal的距离时,
                # 把前面满足小于等于midVal的距离对全部添加上
                count += (j - i - 1)
                i += 1
            elif j == len(nums):  # 当j已经遍历到头时,与上面满足统计条件的操作一致,可以合并
                count += (j - i - 1)
                i += 1
        return count


if __name__ == "__main__":
    obj = Solution()
    while True:
        try:
            in_line = input().strip().split('=')
            nums = [int(n) for n in in_line[1].split('[')[1].split(']')[0].split(',')]
            k = int(in_line[2])
            print(obj.smallestDistancePair(nums, k))
        except EOFError:
            break

个人总结体会

通过刷上述几道题目,除了掌握了基于值域的二分法步骤和模板,也同时掌握了对于行列递增的矩阵、两个递增的数组求和、单个递增的数组求元素对距离 这种“特殊序列”里用双指针法确定小于等于某一数值的元素或组合个数。

你可能感兴趣的:(数据结构与算法,leetcode)