hard

Smallest Range Covering Elements From K Lists

Hold one number per sorted list in a min-heap and repeatedly advance the smallest to shrink the covering range.

1. Define the problem

Smallest Range Covering Elements From K Lists

You are given k lists of whole numbers, and every list is already sorted from smallest to largest. Find the smallest range [a, b] that contains at least one number from every list. A range [a, b] is smaller than [c, d] when b - a is smaller than d - c. If two ranges are the same width, the one starting earlier wins. Keep a pointer into each list , all starting at position 0, and hold the k numbers those pointers sit on in a min-heap. Those k numbers already touch every list, so the range from the smallest of them to the largest of them is a valid answer. Record it, then move the pointer that produced the smallest number forward by one and try again. When any list runs out, no better range can exist.

Constraints

  • 1 ≤ nums.length ≤ 3500
  • 1 ≤ numsi.length ≤ 50
  • Each numsi is sorted from smallest to largest
  • -105 ≤ numsi[j] ≤ 105

Example

Inputnums = [[1, 9], [8, 12], [6, 7]]
Output[7, 9]

Explanation The range from 7 to 9 contains 9 from the first list, 8 from the second list and 7 from the third list, and it is only 2 wide. No narrower range covers all three lists.

2. Know the words first

In plain terms

Heap
A container that always knows its best item, where best means smallest or largest depending on how you set it up. Adding an item or taking the best item out costs about log n steps, and you never have to sort the whole collection.
Backing array
A heap is stored as one plain list. The item at position i keeps its parent at position (i - 1) / 2 rounded down, and its two children at positions 2i + 1 and 2i + 2. That is why every picture below is a row of numbered boxes.
Min-heap
A heap whose best item is the smallest one. Each item here is a number together with which list it came from and where in that list it sits.
Pointer into a list
A position number saying how far you have walked into that list. Moving a pointer forward means looking at the next, larger, number in the same list.
Why the largest is tracked separately
A min-heap only tells you its smallest item cheaply. The largest of the k current numbers is kept in its own variable, updated whenever a bigger number is pushed in, because numbers only ever grow as pointers move forward.
3. Visualize the solution

The row of boxes is the backing array of the min-heap, holding the number each list pointer is currently on

The row of boxes is the backing array of the min-heap, holding the number each list pointer is currently on
Statusinit

Push the first number of each list. The heap holds 1, 8 and 6.

What happens in this step

pointers all at position 0
backing array = [1, 8, 6]
highest = 8
best range so far = [1, 8], width 7

Slot 0 holds 1, the smallest of the three. These three numbers already
cover all three lists, so [1, 8] is a valid starting answer.
Step 1 of 5

Steps to visualize

  1. The lists are list 0 = [1, 9], list 1 = [8, 12] and list 2 = [6, 7].
  2. Each box is one slot of the list that stores the heap. There is always exactly one number per input list, so the heap stays 3 items wide.
  3. The largest of the three numbers is tracked in a separate variable called highest.
  4. Each round: pop the smallest, compare highest minus that smallest against the best range so far, then push the next number from the same list.
  5. The moment a list has no next number, the search stops.
4. Walk through the code

Walk through the code

Same walkthrough, now with the code. Press Next to move one step and watch which lines run.

The row of boxes is the backing array of the min-heap, holding the number each list pointer is currently on
Statusinit

Push the first number of each list. The heap holds 1, 8 and 6.

What happens in this step

pointers all at position 0
backing array = [1, 8, 6]
highest = 8
best range so far = [1, 8], width 7

Slot 0 holds 1, the smallest of the three. These three numbers already
cover all three lists, so [1, 8] is a valid starting answer.
Step 1 of 5
5. Solution

Solution

solution.tsTypeScript
function smallestRange(nums) {
  const heap = new Heap((a, b) => a[0] - b[0]);
  let highest = -Infinity;

  for (let list = 0; list < nums.length; list++) {
    heap.push([nums[list][0], list, 0]);
    highest = Math.max(highest, nums[list][0]);
  }

  let start = heap.peek()[0];
  let end = highest;

  while (true) {
    const smallest = heap.pop();

    if (highest - smallest[0] < end - start) {
      start = smallest[0];
      end = highest;
    }

    const nextIndex = smallest[2] + 1;
    if (nextIndex === nums[smallest[1]].length) return [start, end];

    const nextValue = nums[smallest[1]][nextIndex];
    highest = Math.max(highest, nextValue);
    heap.push([nextValue, smallest[1], nextIndex]);
  }
}

class Heap {
  constructor(compare) {
    this.items = [];
    this.compare = compare;
  }

  size() {
    return this.items.length;
  }

  peek() {
    return this.items[0];
  }

  push(value) {
    this.items.push(value);
    let child = this.items.length - 1;

    while (child > 0) {
      const parent = (child - 1) >> 1;
      if (this.compare(this.items[child], this.items[parent]) >= 0) break;
      const swap = this.items[child];
      this.items[child] = this.items[parent];
      this.items[parent] = swap;
      child = parent;
    }
  }

  pop() {
    const top = this.items[0];
    const last = this.items.pop();

    if (this.items.length > 0) {
      this.items[0] = last;
      let parent = 0;

      while (true) {
        const left = parent * 2 + 1;
        const right = parent * 2 + 2;
        let best = parent;

        if (left < this.items.length && this.compare(this.items[left], this.items[best]) < 0) best = left;
        if (right < this.items.length && this.compare(this.items[right], this.items[best]) < 0) best = right;
        if (best === parent) break;

        const swap = this.items[parent];
        this.items[parent] = this.items[best];
        this.items[best] = swap;
        parent = best;
      }
    }

    return top;
  }
}
Time
O(n log k) where n is the total count of numbers and k is the number of lists
Space
O(k)
6. Test cases

Test cases

InputExpectedCovers
nums = [[1, 9], [8, 12], [6, 7]][7, 9]example from the docstring
nums = [[4, 10, 15, 24, 26], [0, 9, 12, 20], [5, 18, 22, 30]][20, 24]larger lists where the answer sits in the middle
nums = [[1, 2, 3], [1, 2, 3], [1, 2, 3]][1, 1]duplicate values across lists give a range of width zero
nums = [[1], [2], [3]][1, 3]every list runs out immediately, so the first range is the answer
nums = [[1, 2, 3, 4, 5]][1, 1]degenerate case with a single list
nums = [[-5, -2, 0], [-1, 3], [2, 4]][-1, 2]negative values mixed with positive ones