Facebook Pixel

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 from nums1
  • v: one element from nums2
  • 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 sum nums1[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 in nums1
  • 0: the index in nums2 (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 Evaluator

Example 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 in nums1[:k]), then heapify it, which takes O(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 most min(k, m) elements and each pop-push maintains or decreases the heap size
  • 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 most min(k, m) elements at any time, as we initially add at most k pairs from nums1 (or all of nums1 if it has fewer than k elements)
  • The answer list ans stores exactly k pairs (or fewer), which is O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More