基于值域的二分法与基于定义域的题型不同,它的目标是从一“特殊排序序列”中确定“第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<j所以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
个人总结体会
通过刷上述几道题目,除了掌握了基于值域的二分法步骤和模板,也同时掌握了对于行列递增的矩阵、两个递增的数组求和、单个递增的数组求元素对距离 这种“特殊序列”里用双指针法确定小于等于某一数值的元素或组合个数。