题目描述
思路解析动画文字版
暴力法浪费在“生成了很多根本不会进前 k 的大数对”。我们只需要始终知道当前最小的候选是谁。
这就是多路归并:堆里始终放每条链当前最小的未使用候选,堆顶就是全局下一个最小数对。
把每一行看成递增链:每个堆节点显示 sum,节点下方标注 i、j 和真实数对。固定 nums1[0]=1 时,和是 3、5、7;固定 nums1[1]=7 时,和是 9、11、13。每行内部递增,所以一开始只需要拿每行第一个候选参赛。
初始化堆:放入行 0 的第一个候选:堆元素存 (sum, i, j)。sum 用来排序,i 和 j 用来回到 nums1[i]、nums2[j] 拿具体数对。
继续初始化:放入行 1:行 1 的第一个候选是 [7,2],和为 9。堆顶仍然是和为 3 的 [1,2]。
初始化完成:最多放 min(k, len(nums1)) 行:k=3,nums1 也只有 3 行,所以放入三条链的第一个候选。若 nums1 很长,也最多放 k 行:第 k 行及以后的首候选和,大于等于前 k 行首候选里的最大和,不会比这 k 个首候选更早进入前 k 个答案。
第 1 次弹堆:拿到 [1,2]:堆顶一定是当前所有链里最小的候选,所以 [1,2] 可以确定进入答案。画面里的堆已经移除了 sum=3 的根节点。
推进同一行:把 [1,4] 入堆:弹出 (i,j) 后,只推进同一行的 (i,j+1)。不能乱推进下一行,因为下一行的第一个候选已经在堆里了。
第 2 次弹堆:拿到 [1,4]:[1,4] 是当前最小候选。画面已经移除了 sum=5,剩下的堆顶变成 [7,2],它的和是 9。
继续推进行 0:把 [1,6] 入堆:因为 nums2 升序,行 0 的候选按 3、5、7 递增。弹出 5 后,7 成为这一行新的最小未使用候选。
第 3 次弹堆:拿到 [1,6]:这已经收集到 k=3 个答案。画面已经移除了 sum=7,堆里还剩 [7,2]、[11,2],但题目只要前 3 个。
行 0 到尾部,不再推进:刚弹出的 (0,2) 已经是行 0 最后一格;nums2 长度为 3,下标范围是 0..2,所以行 0 耗尽,不再入堆。堆里虽然还剩行 1、行 2 的候选,但 len(ans)==k,while 条件不成立,循环结束。
看到多个有序来源要取整体前 k 小,先想“每路放一个候选进小根堆”。
参考代码
import heapqclass Solution: def kSmallestPairs(self, nums1, nums2, k): if not nums1 or not nums2 or k == 0: return [] heap = [] for i in range(min(k, len(nums1))): heapq.heappush(heap, (nums1[i] + nums2[0], i, 0)) ans = [] while heap and len(ans) < k: _, 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复杂度
- 时间复杂度:O(k log min(k, m)),m=len(nums1)。初始堆最多 min(k,m) 个元素,最多弹出 k 次,每次堆操作取对数。
- 空间复杂度:O(min(k, m)),堆里最多保存每条活跃配对链的一个候选。
易错点
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
前K个高频单词
LeetCode 692 · 中等 · 沿着 堆套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题