TL;DR
Min-heap frontier over the sorted sum-grid: O(k log k) time, O(k) space.
Approach 1 β Brute force (enumerate all pairs)
Generate every (u, v) pair, sort by sum, keep the first k.
from typing import List
class Solution:
def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
pairs = [[u, v] for u in nums1 for v in nums2]
pairs.sort(key=sum)
return pairs[:k]
O(mn log(mn)) time and O(mn) space. With m = n = 10^5 thatβs 10^10 pairs β hundreds of gigabytes before the sort even starts. The constraints exist precisely to kill this.
Approach 2 β Min-heap over the candidate frontier
The insight: view the sums as an mΓn grid G[i][j] = nums1[i] + nums2[j]. Sorted inputs make every row and column non-decreasing, so G[i][j] can only be the next-smallest after its up-neighbor and left-neighbor are already taken. We can therefore walk the grid like a best-first search (Dijkstra-style frontier): keep a min-heap of candidate cells, always pop the smallest, and only then reveal its neighbors.
The tidy version pushes one candidate per row: seed (i, 0) for the first min(k, m) rows (a pair in row i can never be in the answer before that rowβs first column), and after popping (i, j) push only (i, j + 1). Each row feeds the heap left-to-right, so no cell is pushed twice and no visited-set is needed.
import heapq
from typing import List
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)
result: List[List[int]] = []
while heap and len(result) < k:
_, i, j = heapq.heappop(heap)
result.append([nums1[i], nums2[j]])
if j + 1 < len(nums2):
heapq.heappush(heap, (nums1[i] + nums2[j + 1], i, j + 1))
return result
Walkthrough of nums1 = [1, 7, 11], nums2 = [2, 4, 6], k = 3:
- Seed rows 0..2: heap = {(3, 0,0), (9, 1,0), (13, 2,0)}.
- Pop (3, 0,0) β answer
[1, 2]; push (5, 0,1). Heap {5, 9, 13}.
- Pop (5, 0,1) β answer
[1, 4]; push (7, 0,2). Heap {7, 9, 13}.
- Pop (7, 0,2) β answer
[1, 6]; row 0 exhausted, nothing pushed.
- 3 pairs collected β return
[[1, 2], [1, 4], [1, 6]]. β
And for the duplicate case nums1 = [1, 1, 2], nums2 = [1, 2, 3], k = 2: seed {(2, 0,0), (2, 1,0), (3, 2,0)} β the two index-distinct (1, 1) pairs are separate heap entries, so the answer is [[1, 1], [1, 1]] as required.
Complexity: heap size never exceeds min(k, m); we do k pops and β€ k pushes β O(k log k) time (plus O(min(k, m)) to seed), O(min(k, m)) space. Note tuple comparison falls back to the i, j ints on sum ties β all comparable, no crash.
Approach 3 β Two-neighbor BFS with a visited set (the general grid version)
The insight: the same frontier idea works without the per-row trick: start from cell (0, 0) only, and when popping (i, j) push both neighbors (i+1, j) and (i, j+1), using a visited set to avoid double-pushing a cell reachable two ways. This is the version that generalizes to βk-th smallest in a sorted matrix.β
import heapq
from typing import List
class Solution:
def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
m, n = len(nums1), len(nums2)
heap = [(nums1[0] + nums2[0], 0, 0)]
seen = {(0, 0)}
result: List[List[int]] = []
while heap and len(result) < k:
_, i, j = heapq.heappop(heap)
result.append([nums1[i], nums2[j]])
if i + 1 < m and (i + 1, j) not in seen:
seen.add((i + 1, j))
heapq.heappush(heap, (nums1[i + 1] + nums2[j], i + 1, j))
if j + 1 < n and (i, j + 1) not in seen:
seen.add((i, j + 1))
heapq.heappush(heap, (nums1[i] + nums2[j + 1], i, j + 1))
return result
Tracing the first example: pop (3, 0,0) β push (9, 1,0) and (5, 0,1); pop (5, 0,1) β push (11, 1,1) and (7, 0,2); pop (7, 0,2) β three pairs collected, same answer as before. Each pop adds β€ 2 cells, so the heap holds O(k) entries β O(k log k) time, O(k) space for heap plus visited set. Same asymptotics as Approach 2 but with the extra set bookkeeping β know both; interviewers ask for the visited-set variant when the data is a matrix rather than two arrays.
Common pitfalls
- Seeding all m rows instead of
min(k, m) β with m = 10^5 and k = 10, youβd heapify 10^5 entries to output 10 pairs.
- Pushing both neighbors without a visited set: cell (i, j) gets pushed once via (iβ1, j) and once via (i, jβ1), duplicating answers.
- Storing pairs of values in the heap without indices β you lose the ability to find βthe next element to the right,β and duplicated values collapse index-distinct pairs.
- Forgetting the βfewer than k pairs existβ case: the loop must stop when the heap empties, not assume k pops always succeed.
Pattern takeaway
When the candidate space is a grid (or any DAG) whose sums are monotone along each axis, donβt materialize it β run best-first search: heap of frontier candidates, pop the min, push only the cells that popping just unlocked. The heap stays O(k)-sized and total work is O(k log k), independent of the mn cells you never looked at. The per-row seeding trick removes the visited-set whenever one axis can be enumerated in order.