Heaps · Heap Fundamentals

Heap Frontier Pair Search

Finding the k best combinations from two sorted sets by exploring outward from the single best combination, using a heap to always expand the most promising still-unvisited neighbor next, and a visited set to avoid re-exploring the same combination twice.

  • 6 min read
  • 9 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: k-th smallest in a sorted matrix
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Two sorted arrays of n values give n^2 pair sums; you want only the k largest. A matrix whose rows and columns are sorted has n^2 values; you want the k-th smallest. Generating everything costs O(n^2) or more - but the sorted structure means the next answer is always adjacent to answers already found.

This skill covers best-first search over a sorted grid: start at the best corner, keep a heap of frontier cells, and after each pop add only the neighbors that could be next. It also covers when a visited set is needed and when seeding by row avoids it, and the binary-search-on-value alternative for large k.

We'll go from simple to more technical:

  1. The pattern: the k largest pair sums, starting at the corner and pushing two neighbors per pop.
  2. A second flavor: the k-th smallest in a row- and column-sorted matrix, seeded with each row's head.
  3. Where top-k combinations show up (ranking, recommendations, top-k joins).
  4. What breaks it: no visited set, the wrong corner, treating the matrix as sorted, the sort's cost.

The approach

Java

int[] maxCombinations(int[] a, int[] b, int k) {
    int[] x = a.clone(), y = b.clone();
    Arrays.sort(x);
    Arrays.sort(y);
    int n = x.length, m = y.length;
    PriorityQueue<int[]> heap = new PriorityQueue<>((p, q) -> Integer.compare(q[0], p[0]));   // max-heap {sum, i, j}
    Set<Long> seen = new HashSet<>();
    heap.offer(new int[]{x[n - 1] + y[m - 1], n - 1, m - 1});      // the largest pair is the last of each
    seen.add((long) (n - 1) * m + (m - 1));
    int[] out = new int[k];
    for (int t = 0; t < k; t++) {
        int[] top = heap.poll();
        out[t] = top[0];
        int i = top[1], j = top[2];
        if (i > 0 && seen.add((long) (i - 1) * m + j)) heap.offer(new int[]{x[i - 1] + y[j], i - 1, j});
        if (j > 0 && seen.add((long) i * m + j - 1)) heap.offer(new int[]{x[i] + y[j - 1], i, j - 1});
    }
    return out;
}

Python

import heapq


def max_combinations(a: list[int], b: list[int], k: int) -> list[int]:
    x, y = sorted(a), sorted(b)
    n, m = len(x), len(y)
    heap = [(-(x[n - 1] + y[m - 1]), n - 1, m - 1)]   # max-heap via negated sums
    seen = {(n - 1, m - 1)}                             # a set of (i, j) tuples - no index encoding needed
    out = []
    for _ in range(k):
        neg, i, j = heapq.heappop(heap)
        out.append(-neg)
        if i > 0 and (i - 1, j) not in seen:
            seen.add((i - 1, j))
            heapq.heappush(heap, (-(x[i - 1] + y[j]), i - 1, j))
        if j > 0 and (i, j - 1) not in seen:
            seen.add((i, j - 1))
            heapq.heappush(heap, (-(x[i] + y[j - 1]), i, j - 1))
    return out

The pairs form a grid where sums shrink as either index decreases. The best pair is the corner (n - 1, m - 1), and every other pair is reachable by stepping down one index at a time - so after each pop, its two "next-best" neighbors join the frontier. A pair can be reached from two neighbors ((i, j) from (i + 1, j) and from (i, j + 1)); without the visited set it's pushed twice and can appear twice in the answer - on the hidden test, 63 shows up twice and the real fifth sum, 56, is lost. Cost: O(n log n) to sort, then O(k log k) for the heap.

A second flavor: k-th smallest in a sorted matrix

A matrix with sorted rows and sorted columns isn't sorted in reading order ([[1, 5], [2, 6]] reads 1, 5, 2, 6), but the same frontier works from the smallest corner. Seed the heap with the head of each row; after popping a cell, push its right neighbor. Each row is entered once, so no visited set is needed:

Java

int kthSmallest(int[][] matrix, int k) {
    int n = matrix.length;
    PriorityQueue<int[]> heap = new PriorityQueue<>((p, q) -> Integer.compare(matrix[p[0]][p[1]], matrix[q[0]][q[1]]));
    for (int r = 0; r < Math.min(n, k); r++) heap.offer(new int[]{r, 0});
    for (int i = 1; i < k; i++) {
        int[] top = heap.poll();
        if (top[1] + 1 < n) heap.offer(new int[]{top[0], top[1] + 1});
    }
    int[] kth = heap.peek();
    return matrix[kth[0]][kth[1]];
}

Python

import heapq


def kth_smallest(matrix: list[list[int]], k: int) -> int:
    n = len(matrix)
    heap = [(matrix[r][0], r, 0) for r in range(min(n, k))]   # the head of each row
    heapq.heapify(heap)
    for _ in range(k - 1):
        _, r, c = heapq.heappop(heap)
        if c + 1 < n:
            heapq.heappush(heap, (matrix[r][c + 1], r, c + 1))
    return heap[0][0]

That's O(k log n). The alternative is binary search on the answer: count values <= mid with a staircase walk from the top-right corner in O(n), and find the smallest value whose count reaches k - O(n log(max - min)), independent of k.

Where you'll see it

Recommendation and search systems combine two ranked lists (candidates x jobs, products x offers) and need only the top few combined scores; database engines compute top-k joins the same way without materializing the full cross product. The same best-first frontier is Dijkstra's algorithm in miniature.

Which tool for which job?

You needUse
The k largest (or smallest) pair sums, k smalla frontier heap from the best corner
The k-th smallest in a sorted matrix, k smalla heap seeded with each row's head
The k-th value when k is largebinary search on the value, counting with a staircase walk
The k best sums of three or more liststhe same frontier, one neighbor per list
Every pair anywaygenerate and sort

Common mistakes

MistakeWhat happensDo this instead
No visited set when a cell has two parentsduplicates pushed and counteda visited set, or seed per row so each cell has one parent
The wrong starting cornerthe first pop isn't the bestlargest at the last indices, smallest at the first
Reading the matrix in row ordermatrix[(k - 1) / n][(k - 1) % n] is wronga heap or binary search
Forgetting the sortthe frontier assumes sorted inputssort first, O(n log n)
(i, j) packed as i * m + j in an intdifferent cells collidea long key, or a tuple in Python

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why does the largest-pair-sum search need a visited set?

Show answer

A pair (i, j) is a neighbor of both (i + 1, j) and (i, j + 1). If both are popped, it's pushed twice and can be popped twice - a duplicate in the answer that pushes a real sum out.

2. Why is the method O(n log n + k log k) and not just O(k log k)?

Show answer

The frontier only works on sorted arrays, and sorting them costs O(n log n). After that, each of the k answers costs a pop and at most two pushes on a heap of O(k) entries.

3. Why doesn't the sorted-matrix version need a visited set?

Show answer

It seeds one entry per row (the head) and only ever moves right. Each cell is reached from exactly one parent - its left neighbor - so it can't be pushed twice.

Key takeaways

  • Sorted along both axes: start at the best corner and expand the frontier with a heap.
  • Mark cells that have two parents as visited, or seed per row so each cell has one parent.
  • Largest pair sums: O(n log n) sort + O(k log k) heap.
  • Row- and column-sorted isn't row-major sorted; heap O(k log n) or binary search on value.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Top k pair sumsfrontier heap from the corner
k-th in sorted matrix, small kheap of row heads
Large kbinary search on value
Three or more listsone neighbor per list

Snippets worth knowing by heart

Java

Set<Long> seen = new HashSet<>();
seen.add((long) i * m + j);                      // long: an int key collides on large grids

Python

heap = [(matrix[r][0], r, 0) for r in range(min(n, k))]   # one parent per cell: no visited set
heapq.heapify(heap)
for _ in range(k - 1):
    _, r, c = heapq.heappop(heap)
    if c + 1 < n:
        heapq.heappush(heap, (matrix[r][c + 1], r, c + 1))

The traps

  • No visited set for two-parent cells: a cell is pushed twice and counted twice. Track visited, or seed one entry per row.
  • The wrong corner: the first pop isn't the best pair. Start at the largest or smallest corner.
  • Row-major reads: in a row- and column-sorted matrix, matrix[(k-1)/n][(k-1)%n] isn't the k-th smallest. Use a heap or binary search.
  • Int-packed keys: i * m + j in an int can collide or overflow. Use a long key, or a tuple in Python.

Staff-level points, one line each

  • Pops come out in order because expansions are never better (the Dijkstra argument); it matched brute force on 300 cases.
  • n = 100,000 per list, k = 1,000: heap peak 67, visited 1,063, out of 10^10 pairs.
  • Three lists: one neighbor per list, plus a visited set; it matched brute force.
  • Binary search on the value: 42,000 steps for n = 1,000 whatever k is; the heap needed 499,999 pops for the median.
  • With m = 100,000, int keys for (0, 32704) and (42950, 0) were both 32704.

Say it in an interview

  • "Start at the best corner and expand only neighbors; a heap gives the next best."
  • "A cell can have two parents, so I keep a visited set, or seed one per row."
  • "If k is huge I'd binary search the value and count with two pointers."
  • "It's best-first search, and it's correct for the same reason Dijkstra is."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Heap Frontier Pair Search free

Kth Smallest in a Sorted Matrix

MediumProCoding problemJava · Python

Every row and every column of this n x n matrix is sorted ascending. Return the k-th smallest value in the whole matrix, counting repeats.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Maximum Sum Combination

HardProCoding problemJava · Python

Given two arrays a and b, each of size n, and an integer k, return the k largest possible sums of the form a[i] + b[j] (one index from each array; the indices don't need to relate to each other), sorted descending.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Heap Frontier Pair Search free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime