Solution: Find K Pairs with Smallest Sums
Let's solve the Find K Pairs with Smallest Sums problem using the K-Way Merge pattern.
Statement
Given two lists and an integer , find pairs of numbers with the smallest sum so that in each pair, each list contributes one number to the pair.
Constraints:
-
list1.length
,list2.length
-
list1[i]
,list2[i]
- Input lists should be sorted in ascending order.
- If the value of exceeds the total number of valid pairs that may be formed, return all the pairs.
Solution
So far, you’ve probably brainstormed some approaches and have an idea of how to solve this problem. Let’s explore some of these approaches and figure out which one to follow based on considerations such as time complexity and any implementation constraints.
Naive approach
One way to solve this problem is by creating all possible pairs from the given lists. This creation of pairs costs , where and are the lengths of two lists.
Note: Since we’re not certain whether or not the lengths of the two lists are the same, we’ll use and for the first and second list, respectively.
Once we’ve created all pairs, sort them according to their sums, and remove the first pairs from the list. Sorting and removal of pairs costs us .
-
Step 1: Let’s take two lists as an example, and . We can make pairs from these two lists, and those are the following:
-
Step 2: Now, let’s sort the pairs with the order of their sums:
-
Step 3: Let’s remove the first pairs from the sorted list of pairs above and return our output.
Suppose is , so the first pairs with the smallest sums will be the following:
Overall, this approach of solving the pattern costs us time complexity. The space complexity is .
Optimized approach using k-way merge
We can use the k-way merge pattern here because it helps solve problems where we need to compute a result by comparing the elements of multiple sorted lists.
While traversing through the elements of both lists and making pairs, we use a min-heap to track the pair with the smallest sum encountered so far.
As proposed above, we use a min-heap to store three things:
-
The smallest sum (at the root of the min-heap).
-
The sum of each pair.
-
The list indexes of each element in the pair.
The intuition behind this approach involves managing combinations and their sums. The algorithm starts by pairing the first element of the second list with each element of the first list and storing these pairs in a min-heap based on their sums. The pair with the smallest sum will be at the top of the heap. After pushing these pairs in the min-heap with their sums, we pop the pair with the smallest sum and add a new pair by moving one step ahead in the second list. The popped pair will have the smallest sum because the input lists are already in ascending order. This popped pair is stored in the resultant list, and this process of popping pairs and pushing new ones is repeated kk times. Finally, the resultant list will have the pairs with the smallest sums.
Let’s take two lists as an example, and . We can make 3 pairs in the first iteration and store them in the min-heap:
Now, we start a second loop that iterates while elements are in the min-heap and while we have yet to find all smallest pairs. In each iteration, we perform three steps:
-
Pop the smallest pair from the min-heap, noting the sum of the pair and the list indexes of each element, calling the
i
andj
indexes, respectively. -
Add this pair to the result list.
-
Increment
j
to move forward in the second list, and compose a new pair with the same element from the first list and the next element in the second list. This step is skipped if a new pair can’t be formed when the popped pair’sj
was already pointing to the end of the second list.
Supposing in our example, the sequence of pairs pushed and popped in the second step is as follows:
1. Pop: (2 + 1) = 3 // 1st pair // Min-heap: [(8 + 1) = 9, (9 + 1) = 10]
2. Push: (2 + 3) = 5 // Min-heap: [(2 + 3) = 5, (8 + 1) = 9, (9 + 1) = 10]
3. Pop: (2 + 3) = 5 // 2nd pair // Min-heap: [(8 + 1) = 9, (9 + 1) = 10]
4. Push: (2 + 6) = 8 // Min-heap: [(2 + 6) = 8, (8 + 1) = 9, (9 + 1) = 10]
5. Pop: (2 + 6) = 8 // 3rd pair // Min-heap: [(8 + 1) = 9, (9 + 1) = 10]
<-- No pair to push as at the end of list 2 -->
6. Pop: (8 + 1) = 9 // 4th pair // Min-heap: [(9 + 1) = 10]
7. Push: (8 + 3) = 11 // Min-heap: [(9 + 1) = 10, (8 + 3) = 11]
8. Pop: (9 + 1) = 10 // 5th pair // Min-heap: [(8 + 3) = 11]
9. Push: (9 + 3) = 12 // Min-heap: [(8 + 3) = 11, (9 + 3) = 12]
10. Pop: (8 + 3) = 11 // 6th pair // Min-heap: [(9 + 3) = 12]
11. Push: (8 + 6) = 14 // Min-heap: [(9 + 3) = 12, (8 + 6) = 14]
12. Pop: (9 + 3) = 12 // 7th pair // Min-heap: [(8 + 6) = 14]
13. Push: (9 + 6) = 15 // Min-heap: [(8 + 6) = 14, (9 + 6) = 15]
14. Pop: (8 + 6) = 14 // 8th pair // Min-heap: [(9 + 6) = 15]
<-- No pair to push as at the end of list 2 -->
15. Pop: (9 + 6) = 15 // 9th
At all times, the smallest sum is at the root of the min-heap. Overall, we remove pairs from the min-heap.
The slides below illustrate how we would like the algorithm to run:
Let’s look at the code for this solution below:
class Pair {int sum;int i;int j;public Pair(int sum, int i, int j) {this.sum = sum;this.i = i;this.j = j;}}class FindKPairs {public static List<List<Integer>> kSmallestPairs(int[] list1, int[] list2, int k) {List<List<Integer>> pairs = new ArrayList<>();int listLength = list1.length;PriorityQueue<Pair> minHeap = new PriorityQueue<>((a, b) -> a.sum - b.sum);for (int i = 0; i < Math.min(k, listLength); i++) {minHeap.add(new Pair(list1[i] + list2[0], i, 0));}int counter = 1;while (!minHeap.isEmpty() && counter <= k) {Pair pair = minHeap.poll();int i = pair.i;int j = pair.j;pairs.add(Arrays.asList(list1[i], list2[j]));int nextElement = j + 1;if (list2.length > nextElement) {minHeap.add(new Pair(list1[i] + list2[nextElement], i, nextElement));}counter++;}return pairs;}// Driver codepublic static void main(String[] args) {int[][]list1 = {{2, 8, 9},{1, 2, 300},{1, 1, 2},{4, 6},{4, 7, 9},{1, 1, 2}};int[][]list2 = {{1, 3, 6},{1, 11, 20, 35, 300},{1, 2, 3},{2, 3},{4, 7, 9},{1}};int[] k = {9, 30, 1, 2, 5, 4};for(int i=0; i<k.length; i++){List<List<Integer>> result = kSmallestPairs(list1[i], list2[i], k[i]);System.out.print(i+1);System.out.println(".\tInput lists: "+Arrays.toString(list1[i])+ ", "+ Arrays.toString(list2[i]));System.out.println("\tK = "+k[i]);System.out.print("\tPairs with smallest sum are: "+ result);System.out.println("\n");System.out.println(PrintHyphens.repeat("-", 100));}}}
Solution summary
Let’s review the solution we’ve used to solve this problem:
-
We start by pairing only the first element of the second list with each element of the first list. The sum of each pair and their respective indexes from the lists,
i
andj
, are stored on a min-heap. -
Next, we use a second loop in which at each step, we do the following:
-
We pop the pair with the smallest element from the min-heap and collect it in a result list.
-
We make a new pair in which the first element is the first element from the pair we just popped and the second element is the next element in the second list.
-
We push this pair along with its sum onto the min-heap.
-
We keep a count of the steps and stop when the min-heap becomes empty or when we have found pairs.
-
Time complexity
The first loop involves pushing at most pairs onto the heap, each of which takes time. Therefore, the time complexity of the first loop is , where .
The second loop runs at most times, and in each iteration, it involves pushing and popping elements from the heap. Each push and pop operation on the heap costs while the size of the heap remains constant at throughout the loop. Therefore, the time complexity of the second loop is .
The total time complexity is the sum of the complexities of the first and second loops . Since is the minimum of and , is at most . Therefore, we can simplify the overall time complexity to .
Space complexity
The space complexity of this algorithm is , where .
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.