在问 · 算法题解

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

中等栈与堆最小堆多路归并力扣原题 ↗

题目描述

输入两个按非递减顺序排好的整数数组 nums1、nums2,以及一个整数 k。

从 nums1 取一个数 u、从 nums2 取一个数 v,组成数对 [u, v]。不同位置上的元素视为不同的选择,所以值相同的数对可能出现多次。

在所有可能的数对中,找出 u + v 最小的 k 个,按和从小到大的顺序返回。和相同的数对之间先后不限。

示例 1
输入:nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出:[[1,2],[1,4],[1,6]]

解释:和最小的三对是 [1,2]、[1,4]、[1,6],和分别为 3、5、7。

示例 2
输入:nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出:[[1,1],[1,1]]

解释:nums1 的两个 1 分别和 nums2 的 1 配对,得到两个和为 2 的数对。

数据范围

  • 1 <= nums1.length, nums2.length <= 10^5
  • -10^9 <= nums1[i], nums2[i] <= 10^9
  • nums1 和 nums2 都已按升序排列
  • 1 <= k <= 10^4
  • k <= nums1.length * nums2.length

解题思路:最小堆多路归并

把所有数对看成一张表:第 i 行是 nums1[i] 搭配 nums2 的每个数,每一行从左到右和递增。这就像合并 n 条有序链表:先把每行的第一个数对 (i,0) 放进最小堆,每次弹出和最小的 (i,j) 放入答案,再把同一行的下一个 (i,j+1) 推入堆。行数只需取前 min(k, len(nums1)) 行,因为更靠下的行开头更大,前 k 名不可能全落在后面。

为什么正确

堆里每一行最多只有一个候选,而且是该行还没输出的最小数对;所以堆顶就是全部未输出数对中和最小的那个。

复杂度分析

时间 O(k log min(k, m)),空间 O(min(k, m)),m 为 nums1 的长度。

易错点

不要把 (i+1,j) 和 (i,j+1) 都推进堆,那样同一个数对会被重复推入,需要额外去重。只沿行向右推进,初始时放好每行开头即可。另外两个数之和可能超过 32 位整数范围。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        heap = [(nums1[i] + nums2[0], i, 0) for i in range(min(k, len(nums1)))]
        heapq.heapify(heap)
        ans = []
        while heap and len(ans) < k:
            s, i, j = heapq.heappop(heap)
            ans.append([nums1[i], nums2[j]])
            if j + 1 < len(nums2):
                heapq.heappush(heap, (nums1[i] + nums2[j + 1], i, j + 1))
        return ans

载入编辑器,自己跑一遍 →

其他解法

最小堆按和从小到大逐个产出数对,适合 k 较小;二分查找先确定第 k 小的和再一次性收集,空间更省但结果无序。

解法 2:二分查找第 k 小的和 时间 O((m + n) log D + k),空间 O(1)

在数对和的取值范围上二分,找最小的 s,使和不超过 s 的数对至少有 k 个;计数时 i 递增、j 从 nums2 末尾递减,一次扫描完成。然后先收集所有和小于 s 的数对,再补和等于 s 的数对,直到凑满 k 个。D 为最大和与最小和之差。

不需要堆,额外空间 O(1),k 很大时更稳定。输出不按和排序,只保证是和最小的 k 个;代码比堆更长,边界更容易写错。

class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        m, n = len(nums1), len(nums2)

        def count(x):
            cnt, j = 0, n - 1
            for i in range(m):
                while j >= 0 and nums1[i] + nums2[j] > x:
                    j -= 1
                cnt += j + 1
                if cnt >= k:
                    break
            return cnt

        lo, hi = nums1[0] + nums2[0], nums1[-1] + nums2[-1]
        while lo < hi:
            mid = (lo + hi) // 2
            if count(mid) >= k:
                hi = mid
            else:
                lo = mid + 1
        s = lo
        ans = []
        j = n - 1
        for i in range(m):
            while j >= 0 and nums1[i] + nums2[j] >= s:
                j -= 1
            for t in range(j + 1):
                ans.append([nums1[i], nums2[t]])
        j = n - 1
        for i in range(m):
            if len(ans) >= k:
                break
            while j >= 0 and nums1[i] + nums2[j] > s:
                j -= 1
            t = j
            while t >= 0 and nums1[i] + nums2[t] == s and len(ans) < k:
                ans.append([nums1[i], nums2[t]])
                t -= 1
        return ans

栈与堆 专题的其他题目

查看 栈与堆 全部题目 →