基于值域的二分法与基于定义域的题型不同,它的目标是从一“特殊排序序列”中确定“第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 = midValwhile leftVal < rightVal:midVal = leftVal + (rightVal-leftVal) // 2count = self.checkSmallEqualMid(matrix, midVal) # 表示小于等于mid的数量if count < k: # 第k小的数在midVal的右侧,且不包含midValleftVal = midVal + 1elif count >= k: # 第k小的数在midVal的左侧,可能包含midValrightVal = midValreturn leftValdef checkSmallEqualMid(self, matrix: List[List[int]], midVal: int) -> int:n = len(matrix)# 双指针:根据矩阵的特殊性质,从左下角开始统计i, j = n - 1, 0count = 0while i >= 0 and j <= n - 1: # 索引必须在边界之内if matrix[i][j] <= midVal:count += i + 1 # 对应行及行以上全部统计j += 1 # 并更新列数else:i -= 1 # 更新行数return countif __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) // 2count = self.checkSmallEqualMid(nums1, nums2, midVal) # 表示小于等于中间值midVal的数量if count < k: # 第k小的数在midVal的右侧,且不包含midValleftVal = midVal + 1elif count >= k: # 第k小的数在midVal的左侧,可能包含midValrightVal = midValpairSum = leftVal# 第二步,将小于pairSum的数对升序添加到result中# 分为两步添加是为了避免等于pairSum的数对和太多,导致所有小于的没有被添加到result中# 双指针:根据两个数组的特殊性质,i取nums1的最小值位置,j取nums2的最大值位置(以nums1为基准进行)i, j = 0, len(nums2) - 1while 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 resulti += 1elif 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) - 1while i < len(nums1) and j >= 0: # 索引必须在边界之内if nums1[i] + nums2[j] < pairSum:i += 1elif nums1[i] + nums2[j] > pairSum:j -= 1else:t = j # 添加nums2元素时考虑重复情况,同时j的位置要被记录而不能被更新while t >= 0 and nums1[i] + nums2[t] == pairSum:result.append([nums1[i], nums2[t]])if len(result) == k:return resultt -= 1i += 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 -= 1return resultdef checkSmallEqualMid(self, nums1: List[int], nums2: List[int], midVal: int) -> int:# 双指针:根据两个数组的特殊性质,i取nums1的最小值位置,j取nums2的最大值位置i, j = 0, len(nums2) - 1count = 0while i < len(nums1) and j >= 0: # 索引必须在边界之内if nums1[i] + nums2[j] <= midVal:count += j + 1 # 对应nums2位置元素及之前的全部统计i += 1else:j -= 1return countif __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) // 2count = self.countSmallEqual(nums, midVal) # 表示小于等于中间值midVal的数量if count < k: # 第k个数对距离 一定大于midValleftVal = midVal + 1elif count >= k: # 第k个数对距离 可能小于或等于midValrightVal = midValreturn leftValdef countSmallEqual(self, nums: List[int], midVal: int) -> int:# 双指针统计小于等于midVal的数对距离个数count = 0i, j = 0, 1 # i,j指向数组第一、第二位置while i < len(nums) - 1: # i<j所以i不能取到最后一个索引if j < len(nums) and nums[j] - nums[i] <= midVal:j += 1elif j < len(nums) and nums[j] - nums[i] > midVal: # 满足统计条件:索引全部有效时,第一次出现超过midVal的距离时,# 把前面满足小于等于midVal的距离对全部添加上count += (j - i - 1)i += 1elif j == len(nums): # 当j已经遍历到头时,与上面满足统计条件的操作一致,可以合并count += (j - i - 1)i += 1return countif __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
个人总结体会
通过刷上述几道题目,除了掌握了基于值域的二分法步骤和模板,也同时掌握了对于行列递增的矩阵、两个递增的数组求和、单个递增的数组求元素对距离 这种“特殊序列”里用双指针法确定小于等于某一数值的元素或组合个数。