Coding Interview Patterns Module 13 – Heaps and top-K
Frontier heaps for k-th smallest pairs
The k pairs with the smallest sums from two sorted lists, without pricing the whole grid of pairs: the problem, the brute force and the signal for a heap.
What you will learn
- Find the k smallest pair sums of two sorted lists in O(k log k) time with a frontier heap
- Explore a sorted grid with a heap that holds only the edge of what has been taken
- Prevent a grid search from offering the same pair twice, and compare two ways of doing it
- Prove that the frontier never skips the next smallest pair
- Choose a different method when only the k-th sum is needed and k is huge
Before you start
What you’ll learn · Before you start
What you will learn
- Find the k smallest pair sums of two sorted lists in O(k log k) time with a frontier heap
- Explore a sorted grid with a heap that holds only the edge of what has been taken
- Prevent a grid search from offering the same pair twice, and compare two ways of doing it
- Prove that the frontier never skips the next smallest pair
- Choose a different method when only the k-th sum is needed and k is huge
Before you start
On this page
To find the k pairs with the smallest sums from two sorted lists, picture the sums as a grid: row i, column j holds
a[i] + b[j], and the grid rises along every row and down every column. The smallest pair sits in the corner, and
every later one lies next to a pair already taken, so a heap that holds only that edge, the frontier, hands out the
pairs in order with O(k log k) work instead of pricing all n × m pairs. The same frontier finds the k-th smallest
value of any matrix whose rows and columns are sorted. The problem, its brute force and the signal follow.
The problem: the k cheapest combos on a canteen menu
The problem
A canteen lists its n mains and its m sides, each sorted by price, cheapest first. A combo is one main and one
side, (i, j), and costs a[i] + b[j]. Return the k cheapest combos, cheapest first; combos with the same price
come in order of i, then of j. Limits: 1 ≤ n, m ≤ 100,000; 1 ≤ k ≤ 10,000, and when k exceeds n × m, every combo
is returned; prices from 1 to 10⁹.
- Mains
[100, 130, 200], sides[20, 50, 90]and k = 5 give (0, 0) 120, (0, 1) 150, (1, 0) 150, (1, 1) 180 and (0, 2) 190. The two combos at 150 come in order of the main. - Mains
[5], sides[1, 1]and k = 3 give (0, 0) and (0, 1): only two combos exist.
The brute force, and why it fails at these sizes
Price every combo, sort them all and keep the first k. It is correct, but at the limits the grid has n × m = 10¹⁰ cells: 80 GB just to hold their prices as 8-byte numbers, then a sort of 10¹⁰ items to keep 10,000 of them. A heap of size k over all the combos saves the memory, yet it still prices every one of the 10¹⁰.
# The brute force: price every (main, side) combo, sort them all and keep the first k. Correct, and it shows what
# the answer must look like, but the work is the full grid of combos.
def k_cheapest_brute(mains, sides, k):
combos = [(m + s, i, j) for i, m in enumerate(mains) for j, s in enumerate(sides)]
combos.sort() # by price, then by the main's position, then by the side's
return combos[:k], len(combos)
for mains, sides, k in (([100, 130, 200], [20, 50, 90], 5), ([5], [1, 1], 3)):
best, priced = k_cheapest_brute(mains, sides, k)
print(f"k = {k}:", ", ".join(f"({i}, {j}) {price}" for price, i, j in best), f"[{priced} combos priced]")
n = m = 100_000
print(f"at the limits: {n:,} mains x {m:,} sides = {n * m:,} combos to price and sort, to keep k = 10,000")
print(f"the prices alone, as 8-byte numbers: {n * m * 8 / 10**9:,.0f} GB") Output
k = 5: (0, 0) 120, (0, 1) 150, (1, 0) 150, (1, 1) 180, (0, 2) 190 [9 combos priced] k = 3: (0, 0) 6, (0, 1) 6 [2 combos priced] at the limits: 100,000 mains x 100,000 sides = 10,000,000,000 combos to price and sort, to keep k = 10,000 the prices alone, as 8-byte numbers: 80 GB
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
What to notice
The price grid after the three cheapest combos
Text description of the diagram
The grid shows the price of every combo of the canteen example: main i down the rows, with prices 100, 130 and 200, and side j across the columns, with prices 20, 50 and 90. Each cell holds its position (i, j) and its price, the sum of the two.
Row 0 holds (0, 0) 120, (0, 1) 150 and (0, 2) 190. Row 1 holds (1, 0) 150, (1, 1) 180 and (1, 2) 220. Row 2 holds (2, 0) 220, (2, 1) 250 and (2, 2) 290. Prices rise along every row and down every column.
The three cheapest combos, (0, 0) at 120, (0, 1) at 150 and (1, 0) at 150, are marked taken and drawn with a double border. The three cells next to them, (0, 2) at 190, (1, 1) at 180 and (2, 0) at 220, are marked candidate. The other three, (1, 2), (2, 1) and (2, 2), are marked not reached and drawn dashed.
Below the grid, a note says that because rows and columns rise, every candidate is next to a taken combo, and the next cheapest combo is the candidate at 180.
- The grid is sorted twice. Prices rise along every row, because the sides are sorted, and down every column, because the mains are.
- So a combo can be the next cheapest only once the combo to its left and the one above it are taken. The candidates form a thin edge around the taken corner, never the whole grid.
Keeping that edge in a min-heap and taking its cheapest combo again and again is the pattern of this lesson. After three combos, the edge in the example holds three candidates, and the cheapest of them, 180, comes next.
The frontier, step by step: rows started on demand Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Two other frontiers, and the seen set one of them needs
- Why the next cheapest combo is always in the heap: the invariant and a proof
- A dry run on the canteen menu
- Measured: three frontiers on 100,000 mains and 100,000 sides
- When only the k-th price is needed: binary search on the price
- The code in five languages
- Edge cases and common bugs, each found by a drill
- Variants: sorted matrices, three lists and the dearest combos
- Exercise: the k cheapest combos of two sorted menus
- The k cheapest combos of two sorted menus
- A practice set with sample answers
- Check yourself
- Interview questions
- Why does the grid of sums a[i] + b[j] of two sorted lists rise along every row and down every column, and what does that tell you about where the next smallest sum can be?
- A k-smallest-pairs solution starts at (0, 0) and pushes both neighbours, (i + 1, j) and (i, j + 1), of every pair it pops. On some inputs it returns the same pair twice. Explain why, and give two fixes with their costs.
- Two sorted lists hold 100,000 values each, from 1 to 10⁹. You need the k-th smallest pair sum for k up to 10¹⁰, and nothing else. Which method do you choose, what does it cost, and how do you know the value it returns is a real pair sum?
- Mains, sides and drinks are each sorted by price, with up to 100,000 items in each list. Return the k cheapest meals of one item from each, for k up to 10,000. Extend the frontier, and compare it with any simpler method you can find.
- Key takeaways
6 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 25 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- heapq: Heap queue algorithm (Python Software Foundation)
- Java SE 25 API: PriorityQueue (Oracle)
- Java SE 25 API: Comparator (Oracle)
- C++ working draft N4950: Class template priority_queue [priority.queue] (ISO/IEC JTC1/SC22/WG21 (HTML rendering of the working draft))
- Package container/heap (The Go Authors)
- The Go Programming Language Specification: integer overflow (The Go Authors)
- ECMAScript 2026 Language Specification: Number.MAX_SAFE_INTEGER (Ecma International)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress