373. Find K Pairs with Smallest Sums
Problem Description
You have two arrays of integers, nums1
and nums2
, that are already sorted in non-decreasing order (smallest to largest). You also have an integer k
.
Your task is to find pairs by taking one element from nums1
and one element from nums2
. Among all possible pairs, you need to return the k
pairs that have the smallest sums.
A pair (u, v)
consists of:
u
: one element fromnums1
v
: one element fromnums2
- The sum of the pair is
u + v
You need to return exactly k
pairs with the smallest sums, formatted as (u₁, v₁), (u₂, v₂), ..., (uₖ, vₖ)
.
For example, if nums1 = [1, 7, 11]
and nums2 = [2, 4, 6]
, some possible pairs would be:
(1, 2)
with sum = 3(1, 4)
with sum = 5(1, 6)
with sum = 7(7, 2)
with sum = 9- And so on...
If k = 3
, you would return the 3 pairs with the smallest sums: [[1,2], [1,4], [1,6]]
.
The solution uses a min-heap to efficiently track and retrieve pairs with the smallest sums. It starts by pairing each element from nums1
(up to k
elements) with the first element of nums2
, then progressively explores pairs with larger sums by moving through nums2
for each element in nums1
.
Intuition
Since both arrays are sorted, we know that the smallest possible sum must be nums1[0] + nums2[0]
. But what about the second smallest? It could be either nums1[0] + nums2[1]
or nums1[1] + nums2[0]
.
This observation leads us to think about the problem geometrically. Imagine a 2D grid where:
- Rows represent elements from
nums1
- Columns represent elements from
nums2
- Each cell
(i, j)
contains the sumnums1[i] + nums2[j]
Because both arrays are sorted, this grid has a special property: values increase as you move right (increasing j
) or down (increasing i
). The smallest value is at the top-left corner (0, 0)
.
To find the k smallest sums efficiently, we can't examine all possible pairs (that would be O(m*n)
). Instead, we need a strategy to explore only the most promising candidates.
The key insight is that for any cell (i, j)
we've already selected, the next smallest sums involving index i
from nums1
must come from (i, j+1)
. Similarly, the next smallest sums involving index j
from nums2
could come from (i+1, j)
.
A min-heap is perfect for this because it always gives us the current smallest unexplored sum. We start by adding all pairs of form (nums1[i], nums2[0])
for i
from 0
to min(k-1, len(nums1)-1)
to the heap. This gives us all potential starting points.
When we pop a pair (i, j)
from the heap, we know it's the next smallest sum. We then push (i, j+1)
to the heap if it exists, which represents the next candidate from that row. This way, we explore the grid in a controlled manner, always processing the smallest available sum next, without having to examine all possible pairs.
The reason we only move right (increasing j
) and not down (increasing i
) is to avoid duplicates - we've already added all necessary starting points from nums1
initially, so we only need to explore horizontally from each starting point.
Solution Approach
The solution uses a min-heap to efficiently find the k smallest pairs. Let's walk through the implementation step by step:
1. Initialize the Min-Heap
q = [[u + nums2[0], i, 0] for i, u in enumerate(nums1[:k])]
heapify(q)
We create initial heap entries by pairing each element from nums1
(up to k
elements) with the first element of nums2
. Each heap entry contains:
u + nums2[0]
: the sum of the pair (used as the priority key)i
: the index innums1
0
: the index innums2
(starting at 0)
We limit to nums1[:k]
because we only need k pairs total, so we don't need more than k starting points.
2. Extract k Smallest Pairs
ans = [] while q and k > 0: _, i, j = heappop(q) ans.append([nums1[i], nums2[j]]) k -= 1
We repeatedly extract the minimum element from the heap. The heap automatically gives us the pair with the smallest sum. We reconstruct the actual pair [nums1[i], nums2[j]]
and add it to our answer list.
3. Add Next Candidate
if j + 1 < len(nums2):
heappush(q, [nums1[i] + nums2[j + 1], i, j + 1])
After extracting a pair (i, j)
, we add the next potential pair from the same row: (i, j+1)
. This represents pairing the same element from nums1
with the next element from nums2
. We only add it if j + 1
is within bounds.
Why This Works:
- The heap maintains the invariant that we always process the smallest unexplored sum next
- By starting with all
(i, 0)
pairs, we ensure every row of our conceptual grid is represented - By only moving right (incrementing
j
), we avoid generating duplicate pairs - The heap size never exceeds
min(k, len(nums1))
because we only add one new element for each element we remove
Time Complexity: O(k log min(k, len(nums1)))
- we perform k heap operations, each taking O(log min(k, len(nums1)))
time.
Space Complexity: O(min(k, len(nums1)))
for the heap storage.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with nums1 = [1, 7, 11]
, nums2 = [2, 4, 6]
, and k = 3
.
Step 1: Initialize the heap
We create initial pairs by combining each element from nums1
(up to k elements) with nums2[0]
:
(1, 2)
with sum = 3, stored as[3, 0, 0]
(7, 2)
with sum = 9, stored as[9, 1, 0]
(11, 2)
with sum = 13, stored as[13, 2, 0]
Initial heap (min-heap based on sum):
Heap: [[3, 0, 0], [9, 1, 0], [13, 2, 0]]
Step 2: Extract first minimum (k=3)
Pop [3, 0, 0]
from heap → Add pair [1, 2]
to answer
Now we push the next candidate from this row: (0, 1)
→ [1+4, 0, 1]
= [5, 0, 1]
Answer: [[1, 2]] Heap: [[5, 0, 1], [9, 1, 0], [13, 2, 0]]
Step 3: Extract second minimum (k=2)
Pop [5, 0, 1]
from heap → Add pair [1, 4]
to answer
Push next candidate: (0, 2)
→ [1+6, 0, 2]
= [7, 0, 2]
Answer: [[1, 2], [1, 4]] Heap: [[7, 0, 2], [9, 1, 0], [13, 2, 0]]
Step 4: Extract third minimum (k=1)
Pop [7, 0, 2]
from heap → Add pair [1, 6]
to answer
Since k is now 0, we stop.
Final Answer: [[1, 2], [1, 4], [1, 6]]
The algorithm efficiently found the 3 pairs with smallest sums (3, 5, 7) without examining all 9 possible pairs. The heap ensured we always processed the smallest available sum next.
Solution Implementation
1from typing import List
2from heapq import heapify, heappop, heappush
3
4class Solution:
5 def kSmallestPairs(
6 self, nums1: List[int], nums2: List[int], k: int
7 ) -> List[List[int]]:
8 """
9 Find k pairs with smallest sums from two sorted arrays.
10
11 Args:
12 nums1: First sorted array
13 nums2: Second sorted array
14 k: Number of pairs to return
15
16 Returns:
17 List of k pairs with smallest sums
18 """
19 # Initialize min heap with pairs formed by each element in nums1 (up to k elements)
20 # paired with the first element of nums2
21 # Heap format: [sum, index_in_nums1, index_in_nums2]
22 min_heap = [[num1 + nums2[0], i, 0] for i, num1 in enumerate(nums1[:k])]
23 heapify(min_heap)
24
25 result = []
26
27 # Extract k smallest pairs from the heap
28 while min_heap and k > 0:
29 # Pop the pair with minimum sum
30 _, index1, index2 = heappop(min_heap)
31
32 # Add the current pair to result
33 result.append([nums1[index1], nums2[index2]])
34 k -= 1
35
36 # If there's a next element in nums2, add new pair to heap
37 # (same element from nums1 with next element from nums2)
38 if index2 + 1 < len(nums2):
39 new_sum = nums1[index1] + nums2[index2 + 1]
40 heappush(min_heap, [new_sum, index1, index2 + 1])
41
42 return result
43
1class Solution {
2 public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
3 // Min heap to store [sum, index1, index2]
4 // Sorted by sum value (ascending order)
5 PriorityQueue<int[]> minHeap = new PriorityQueue<>(
6 Comparator.comparingInt(element -> element[0])
7 );
8
9 // Initialize heap with pairs (nums1[i], nums2[0]) for first k elements of nums1
10 // This ensures we start with smallest possible sums
11 for (int i = 0; i < Math.min(nums1.length, k); i++) {
12 minHeap.offer(new int[] {
13 nums1[i] + nums2[0], // sum
14 i, // index in nums1
15 0 // index in nums2
16 });
17 }
18
19 // Result list to store k smallest pairs
20 List<List<Integer>> result = new ArrayList<>();
21
22 // Extract k smallest pairs from heap
23 while (!minHeap.isEmpty() && k > 0) {
24 // Get the pair with minimum sum
25 int[] currentElement = minHeap.poll();
26 int nums1Index = currentElement[1];
27 int nums2Index = currentElement[2];
28
29 // Add the pair to result
30 result.add(Arrays.asList(nums1[nums1Index], nums2[nums2Index]));
31 k--;
32
33 // Add next pair with same nums1 element but next nums2 element
34 // This maintains the property that we explore pairs in ascending order
35 if (nums2Index + 1 < nums2.length) {
36 minHeap.offer(new int[] {
37 nums1[nums1Index] + nums2[nums2Index + 1], // new sum
38 nums1Index, // same nums1 index
39 nums2Index + 1 // next nums2 index
40 });
41 }
42 }
43
44 return result;
45 }
46}
47
1class Solution {
2public:
3 vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
4 // Custom comparator for min-heap based on sum of pairs
5 // Returns true if sum of pair 'a' is greater than sum of pair 'b'
6 // This creates a min-heap based on the sum
7 auto comparator = [&nums1, &nums2](const pair<int, int>& a, const pair<int, int>& b) {
8 return nums1[a.first] + nums2[a.second] > nums1[b.first] + nums2[b.second];
9 };
10
11 int m = nums1.size();
12 int n = nums2.size();
13 vector<vector<int>> result;
14
15 // Min-heap to store pairs of indices (i, j) where i is index in nums1 and j is index in nums2
16 priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(comparator)> minHeap(comparator);
17
18 // Initialize heap with pairs (i, 0) for first min(k, m) elements from nums1
19 // paired with the first element of nums2
20 for (int i = 0; i < min(k, m); i++) {
21 minHeap.emplace(i, 0);
22 }
23
24 // Extract k smallest pairs
25 while (k-- > 0 && !minHeap.empty()) {
26 // Get the pair with minimum sum
27 auto [index1, index2] = minHeap.top();
28 minHeap.pop();
29
30 // Add the actual values to result
31 result.push_back({nums1[index1], nums2[index2]});
32
33 // If there's a next element in nums2, add the pair (index1, index2 + 1) to heap
34 // This ensures we explore all possible pairs in sorted order
35 if (index2 + 1 < n) {
36 minHeap.emplace(index1, index2 + 1);
37 }
38 }
39
40 return result;
41 }
42};
43
1function kSmallestPairs(nums1: number[], nums2: number[], k: number): number[][] {
2 const m = nums1.length;
3 const n = nums2.length;
4 const result: number[][] = [];
5
6 // Min-heap to store pairs of indices [i, j] where i is index in nums1 and j is index in nums2
7 // Using a custom comparator based on the sum of pairs
8 const minHeap: [number, number][] = [];
9
10 // Comparator function to maintain min-heap property based on sum of pairs
11 const compareFn = (a: [number, number], b: [number, number]): number => {
12 // Compare sums: nums1[a[0]] + nums2[a[1]] vs nums1[b[0]] + nums2[b[1]]
13 return (nums1[a[0]] + nums2[a[1]]) - (nums1[b[0]] + nums2[b[1]]);
14 };
15
16 // Helper function to add element to heap and maintain heap property
17 const heapPush = (item: [number, number]): void => {
18 minHeap.push(item);
19 // Bubble up to maintain min-heap property
20 let currentIndex = minHeap.length - 1;
21 while (currentIndex > 0) {
22 const parentIndex = Math.floor((currentIndex - 1) / 2);
23 if (compareFn(minHeap[currentIndex], minHeap[parentIndex]) < 0) {
24 [minHeap[currentIndex], minHeap[parentIndex]] = [minHeap[parentIndex], minHeap[currentIndex]];
25 currentIndex = parentIndex;
26 } else {
27 break;
28 }
29 }
30 };
31
32 // Helper function to extract minimum element from heap
33 const heapPop = (): [number, number] | undefined => {
34 if (minHeap.length === 0) return undefined;
35 if (minHeap.length === 1) return minHeap.pop();
36
37 const min = minHeap[0];
38 minHeap[0] = minHeap.pop()!;
39
40 // Bubble down to maintain min-heap property
41 let currentIndex = 0;
42 while (true) {
43 const leftChild = 2 * currentIndex + 1;
44 const rightChild = 2 * currentIndex + 2;
45 let smallest = currentIndex;
46
47 if (leftChild < minHeap.length && compareFn(minHeap[leftChild], minHeap[smallest]) < 0) {
48 smallest = leftChild;
49 }
50 if (rightChild < minHeap.length && compareFn(minHeap[rightChild], minHeap[smallest]) < 0) {
51 smallest = rightChild;
52 }
53
54 if (smallest !== currentIndex) {
55 [minHeap[currentIndex], minHeap[smallest]] = [minHeap[smallest], minHeap[currentIndex]];
56 currentIndex = smallest;
57 } else {
58 break;
59 }
60 }
61
62 return min;
63 };
64
65 // Initialize heap with pairs (i, 0) for first min(k, m) elements from nums1
66 // paired with the first element of nums2
67 for (let i = 0; i < Math.min(k, m); i++) {
68 heapPush([i, 0]);
69 }
70
71 // Extract k smallest pairs
72 while (k > 0 && minHeap.length > 0) {
73 // Get the pair with minimum sum
74 const [index1, index2] = heapPop()!;
75
76 // Add the actual values to result
77 result.push([nums1[index1], nums2[index2]]);
78
79 // If there's a next element in nums2, add the pair (index1, index2 + 1) to heap
80 // This ensures we explore all possible pairs in sorted order
81 if (index2 + 1 < n) {
82 heapPush([index1, index2 + 1]);
83 }
84
85 k--;
86 }
87
88 return result;
89}
90
Time and Space Complexity
Time Complexity: O(k * log(min(k, m)))
where m = len(nums1)
and n = len(nums2)
- Initial heap construction: We create a list of at most
min(k, m)
elements (one for each element innums1[:k]
), then heapify it, which takesO(min(k, m))
time - Main loop: We perform exactly
k
iterations (or fewer if we run out of pairs)- Each iteration involves one
heappop
operation:O(log(heap_size))
- Each iteration may involve one
heappush
operation:O(log(heap_size))
- The heap size is bounded by
min(k, m)
because we start with at mostmin(k, m)
elements and each pop-push maintains or decreases the heap size
- Each iteration involves one
- Total:
O(min(k, m)) + O(k * log(min(k, m)))
=O(k * log(min(k, m)))
Space Complexity: O(min(k, m))
- The heap
q
contains at mostmin(k, m)
elements at any time, as we initially add at mostk
pairs fromnums1
(or all ofnums1
if it has fewer thank
elements) - The answer list
ans
stores exactlyk
pairs (or fewer), which isO(k)
space - Since the output space is typically not counted in space complexity analysis, the auxiliary space is
O(min(k, m))
Common Pitfalls
1. Not Handling Empty Arrays
The code assumes both nums1
and nums2
are non-empty. If either array is empty, the code will crash when trying to access nums2[0]
.
Problem Example:
nums1 = [] nums2 = [1, 2, 3] k = 2 # This will cause an IndexError
Solution: Add validation at the beginning of the function:
if not nums1 or not nums2: return []
2. Incorrect Heap Initialization Boundary
A subtle but critical pitfall is not properly limiting the initial heap size. Without nums1[:k]
, if len(nums1)
is very large, we'd initialize the heap with all elements from nums1
, which is inefficient and unnecessary.
Problem Example:
nums1 = [1, 2, 3, ..., 1000000] # Million elements nums2 = [1, 2] k = 3 # Without nums1[:k], we'd create a heap with a million entries unnecessarily
Why the current solution handles this correctly:
The code uses enumerate(nums1[:k])
which properly limits the initial heap size to at most k
elements.
3. Forgetting to Check Bounds for nums2
When pushing new pairs to the heap, forgetting to check if index2 + 1 < len(nums2)
would cause an IndexError.
Problem Example:
# If we didn't have the bounds check:
if index2 + 1 < len(nums2): # This check is essential
heappush(min_heap, [nums1[index1] + nums2[index2 + 1], index1, index2 + 1])
4. Using Wrong Heap Priority
A common mistake is using the wrong value as the heap priority. The heap must be ordered by the sum nums1[i] + nums2[j]
, not by individual values or indices.
Incorrect Implementation:
# Wrong: using just one value as priority
min_heap = [[nums1[i], i, 0] for i in range(min(k, len(nums1)))]
# Wrong: using indices as priority
min_heap = [[i, i, 0] for i in range(min(k, len(nums1)))]
Correct Implementation:
# Correct: using the sum as priority
min_heap = [[nums1[i] + nums2[0], i, 0] for i in range(min(k, len(nums1)))]
5. Not Handling k Larger Than Total Possible Pairs
When k
is larger than the total number of possible pairs (len(nums1) * len(nums2)
), the code should return all possible pairs rather than attempting to find more than exist.
Current solution handles this correctly with the condition while min_heap and k > 0
, which stops when either:
- We've found k pairs (k reaches 0)
- We've exhausted all possible pairs (min_heap is empty)
This prevents the algorithm from running indefinitely or crashing when k exceeds the number of available pairs.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!