Facebook Pixel

632. Smallest Range Covering Elements from K Lists

Problem Description

You are given k lists where each list contains sorted integers in non-decreasing order. Your task is to find the smallest range [a, b] that includes at least one number from each of the k lists.

The problem defines how to compare two ranges:

  • Range [a, b] is considered smaller than range [c, d] if:
    • The width of [a, b] is less than the width of [c, d] (i.e., b - a < d - c), OR
    • If both ranges have the same width (i.e., b - a == d - c), then the range with the smaller starting point is considered smaller (i.e., a < c)

For example, if you have 3 lists:

  • List 1: [1, 5, 8]
  • List 2: [2, 9]
  • List 3: [3, 6, 10]

You need to find a range [a, b] where:

  • The range contains at least one element from List 1
  • The range contains at least one element from List 2
  • The range contains at least one element from List 3
  • Among all such valid ranges, this range is the smallest according to the comparison rules above

The goal is to minimize the range width first, and if there are multiple ranges with the same width, choose the one that starts earliest.

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

Intuition

The key insight is that we need to find a continuous range that covers at least one element from each list. Think of this problem as trying to find a window that "touches" all lists simultaneously.

If we merge all the numbers from all lists into a single sorted array while keeping track of which list each number came from, we can use a sliding window approach. Why does this work?

Consider that any valid range [a, b] must have a and b as actual numbers from our lists (not arbitrary values). The optimal range will have its endpoints as numbers from the lists because we want to minimize the range width.

Once we have all numbers sorted in a single array with their list indices, we can use two pointers to maintain a window. We expand the window by moving the right pointer and include more numbers. As we include numbers, we keep track of which lists are represented in our current window using a counter.

When our window contains at least one number from each of the k lists, we have a valid range. At this point, we can try to shrink the window from the left to find a potentially smaller valid range. We keep shrinking as long as the window remains valid (still contains elements from all k lists).

The sliding window technique works here because:

  1. Once we have a valid window covering all lists, any smaller valid range must either start later (higher left boundary) or end earlier (lower right boundary)
  2. By sorting all numbers first, we ensure we're checking ranges in a systematic way
  3. The two-pointer approach ensures we check all possible valid ranges efficiently without redundant calculations

This approach essentially transforms the problem from dealing with k separate lists into dealing with a single sorted sequence where we need to find the smallest window containing all k different "colors" (list indices).

Learn more about Greedy, Sorting, Sliding Window and Heap (Priority Queue) patterns.

Solution Approach

The solution implements the sorting and sliding window approach discussed in the intuition. Let's walk through the implementation step by step:

Step 1: Create a merged sorted array

t = [(x, i) for i, v in enumerate(nums) for x in v]
t.sort()

We create a list t where each element is a tuple (x, i):

  • x is the actual value from the lists
  • i is the index indicating which list this value came from

After creating this list, we sort it by the values (Python sorts tuples by the first element by default). This effectively merges all k sorted lists into one sorted sequence while preserving the origin information.

Step 2: Initialize data structures

cnt = Counter()
ans = [-inf, inf]
j = 0
  • cnt: A Counter (hash table) to track how many elements from each list are in our current window
  • ans: Stores the best range found so far, initialized to [-inf, inf] to ensure any valid range will be better
  • j: Left pointer for the sliding window (starts at 0)

Step 3: Sliding window implementation

for i, (b, v) in enumerate(t):
    cnt[v] += 1
    while len(cnt) == len(nums):
        # Process valid window

We iterate through the sorted array with i as the right pointer:

  • b is the value at the right end of our window
  • v is which list this value belongs to
  • We add this element to our window by incrementing cnt[v]

Step 4: Process valid windows When len(cnt) == len(nums), we have elements from all k lists in our window:

a = t[j][0]  # Left boundary value
x = b - a - (ans[1] - ans[0])  # Compare range widths
if x < 0 or (x == 0 and a < ans[0]):
    ans = [a, b]
  • a is the value at the left boundary of our window
  • We calculate x as the difference between current range width and best range width
  • Update ans if current range is smaller (negative x) or same width but starts earlier

Step 5: Shrink the window

w = t[j][1]  # Which list the left element belongs to
cnt[w] -= 1
if cnt[w] == 0:
    cnt.pop(w)
j += 1

We try to shrink the window from the left:

  • Decrease the count for the list that the leftmost element belongs to
  • If count becomes 0, remove that list from the counter entirely
  • Move the left pointer forward

The inner while loop continues shrinking as long as we still have all lists represented. Once we lose coverage of any list, we exit the inner loop and continue expanding with the outer loop.

Time Complexity: O(n log n) where n is the total number of elements across all lists, due to sorting.

Space Complexity: O(n) for storing the merged array t and the counter.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with 3 lists:

  • List 1: [4, 10]
  • List 2: [1, 9]
  • List 3: [5, 12]

Step 1: Create merged sorted array We create tuples (value, list_index) and sort them:

Original tuples: [(4,0), (10,0), (1,1), (9,1), (5,2), (12,2)]
Sorted t: [(1,1), (4,0), (5,2), (9,1), (10,0), (12,2)]

Step 2: Initialize variables

  • cnt = {} (empty counter)
  • ans = [-inf, inf] (initial best range)
  • j = 0 (left pointer)

Step 3: Sliding window process

Iteration i=0: Right pointer at (1,1)

  • Add list 1 to window: cnt = {1: 1}
  • Window has 1 list (need 3), continue expanding

Iteration i=1: Right pointer at (4,0)

  • Add list 0 to window: cnt = {1: 1, 0: 1}
  • Window has 2 lists (need 3), continue expanding

Iteration i=2: Right pointer at (5,2)

  • Add list 2 to window: cnt = {1: 1, 0: 1, 2: 1}
  • Window has 3 lists - Valid range found!
  • Current range: [1, 5] (from position j=0 to i=2)
  • Compare with ans: 5-1=4 < inf-(-inf), update ans = [1, 5]
  • Try shrinking: Remove (1,1), cnt = {0: 1, 2: 1}, j=1
  • Only 2 lists now, exit shrinking loop

Iteration i=3: Right pointer at (9,1)

  • Add list 1 to window: cnt = {0: 1, 2: 1, 1: 1}
  • Window has 3 lists - Valid range found!
  • Current range: [4, 9] (from position j=1 to i=3)
  • Compare with ans: 9-4=5 > 5-1=4, keep ans = [1, 5]
  • Try shrinking: Remove (4,0), cnt = {2: 1, 1: 1}, j=2
  • Only 2 lists now, exit shrinking loop

Iteration i=4: Right pointer at (10,0)

  • Add list 0 to window: cnt = {2: 1, 1: 1, 0: 1}
  • Window has 3 lists - Valid range found!
  • Current range: [5, 10] (from position j=2 to i=4)
  • Compare with ans: 10-5=5 > 5-1=4, keep ans = [1, 5]
  • Try shrinking: Remove (5,2), cnt = {1: 1, 0: 1}, j=3
  • Only 2 lists now, exit shrinking loop

Iteration i=5: Right pointer at (12,2)

  • Add list 2 to window: cnt = {1: 1, 0: 1, 2: 1}
  • Window has 3 lists - Valid range found!
  • Current range: [9, 12] (from position j=3 to i=5)
  • Compare with ans: 12-9=3 < 5-1=4, update ans = [9, 12]
  • Try shrinking: Remove (9,1), cnt = {0: 1, 2: 1}, j=4
  • Only 2 lists now, exit shrinking loop

Final Result: The smallest range is [9, 12] with width 3, which includes:

  • 10 from List 1
  • 9 from List 2
  • 12 from List 3

Solution Implementation

1from typing import List
2from collections import Counter
3from math import inf
4
5class Solution:
6    def smallestRange(self, nums: List[List[int]]) -> List[int]:
7        # Create a list of (value, list_index) tuples from all lists
8        # This flattens all lists while keeping track of which list each element came from
9        elements = [(value, list_index) for list_index, list_values in enumerate(nums) for value in list_values]
10      
11        # Sort elements by their values
12        elements.sort()
13      
14        # Counter to track how many lists are represented in current window
15        list_counter = Counter()
16      
17        # Initialize the answer with the widest possible range
18        min_range = [-inf, inf]
19      
20        # Left pointer for sliding window
21        left = 0
22      
23        # Right pointer iterates through sorted elements
24        for right, (right_value, list_index) in enumerate(elements):
25            # Add current element's list to the counter
26            list_counter[list_index] += 1
27          
28            # Try to shrink window from left while all lists are represented
29            while len(list_counter) == len(nums):
30                # Get the left boundary value
31                left_value = elements[left][0]
32              
33                # Calculate range difference
34                current_range_diff = right_value - left_value - (min_range[1] - min_range[0])
35              
36                # Update answer if we found a smaller range
37                # or same range but with smaller starting point
38                if current_range_diff < 0 or (current_range_diff == 0 and left_value < min_range[0]):
39                    min_range = [left_value, right_value]
40              
41                # Remove leftmost element from window
42                left_list_index = elements[left][1]
43                list_counter[left_list_index] -= 1
44              
45                # Remove list from counter if no elements remain from it
46                if list_counter[left_list_index] == 0:
47                    list_counter.pop(left_list_index)
48              
49                # Move left pointer forward
50                left += 1
51      
52        return min_range
53
1class Solution {
2    public int[] smallestRange(List<List<Integer>> nums) {
3        // Calculate total number of elements across all lists
4        int totalElements = 0;
5        for (List<Integer> list : nums) {
6            totalElements += list.size();
7        }
8      
9        // Create array to store all elements with their list index
10        // Each element is stored as [value, listIndex]
11        int[][] elements = new int[totalElements][2];
12        int numLists = nums.size();
13      
14        // Flatten all lists into the elements array
15        int index = 0;
16        for (int listIdx = 0; listIdx < numLists; listIdx++) {
17            for (int value : nums.get(listIdx)) {
18                elements[index++] = new int[] {value, listIdx};
19            }
20        }
21      
22        // Sort elements by their values in ascending order
23        Arrays.sort(elements, (a, b) -> a[0] - b[0]);
24      
25        // Initialize sliding window variables
26        int left = 0;
27        Map<Integer, Integer> listCount = new HashMap<>();  // Track count of elements from each list in window
28        int[] result = new int[] {-1000000, 1000000};  // Initialize with maximum possible range
29      
30        // Sliding window approach: expand window with right pointer
31        for (int[] element : elements) {
32            int rightValue = element[0];
33            int listIndex = element[1];
34          
35            // Add current element's list to the window
36            listCount.merge(listIndex, 1, Integer::sum);
37          
38            // Shrink window from left while it contains at least one element from each list
39            while (listCount.size() == numLists) {
40                int leftValue = elements[left][0];
41                int leftListIndex = elements[left][1];
42              
43                // Check if current range is better than the best found so far
44                int rangeDiff = rightValue - leftValue - (result[1] - result[0]);
45                if (rangeDiff < 0 || (rangeDiff == 0 && leftValue < result[0])) {
46                    // Update result if current range is smaller, or same size but starts earlier
47                    result[0] = leftValue;
48                    result[1] = rightValue;
49                }
50              
51                // Remove leftmost element from window
52                if (listCount.merge(leftListIndex, -1, Integer::sum) == 0) {
53                    listCount.remove(leftListIndex);
54                }
55                left++;
56            }
57        }
58      
59        return result;
60    }
61}
62
1class Solution {
2public:
3    vector<int> smallestRange(vector<vector<int>>& nums) {
4        // Calculate total number of elements across all lists
5        int totalElements = 0;
6        for (auto& list : nums) {
7            totalElements += list.size();
8        }
9      
10        // Create array of (value, listIndex) pairs for all elements
11        vector<pair<int, int>> elements(totalElements);
12        int numLists = nums.size();
13      
14        // Populate the elements array with all values and their corresponding list indices
15        int index = 0;
16        for (int listIdx = 0; listIdx < numLists; ++listIdx) {
17            for (int value : nums[listIdx]) {
18                elements[index++] = {value, listIdx};
19            }
20        }
21      
22        // Sort elements by value (ascending order)
23        sort(elements.begin(), elements.end());
24      
25        // Sliding window approach to find the smallest range
26        int left = 0;  // Left pointer of the window
27        unordered_map<int, int> listCount;  // Count of elements from each list in current window
28        vector<int> result = {-1000000, 1000000};  // Initialize with maximum possible range
29      
30        // Expand window with right pointer
31        for (int right = 0; right < totalElements; ++right) {
32            int rightValue = elements[right].first;
33            int rightListIdx = elements[right].second;
34          
35            // Add current element's list to the window
36            ++listCount[rightListIdx];
37          
38            // Shrink window from left while it contains at least one element from each list
39            while (listCount.size() == numLists) {
40                int leftValue = elements[left].first;
41                int leftListIdx = elements[left].second;
42              
43                // Check if current range is smaller than the best found so far
44                int rangeDiff = rightValue - leftValue - (result[1] - result[0]);
45                if (rangeDiff < 0 || (rangeDiff == 0 && leftValue < result[0])) {
46                    // Update result with the new smaller range
47                    result[0] = leftValue;
48                    result[1] = rightValue;
49                }
50              
51                // Remove leftmost element from window
52                if (--listCount[leftListIdx] == 0) {
53                    listCount.erase(leftListIdx);
54                }
55                ++left;
56            }
57        }
58      
59        return result;
60    }
61};
62
1function smallestRange(nums: number[][]): number[] {
2    // Calculate total number of elements across all lists
3    let totalElements = 0;
4    for (const list of nums) {
5        totalElements += list.length;
6    }
7  
8    // Create array of [value, listIndex] pairs for all elements
9    const elements: [number, number][] = new Array(totalElements);
10    const numLists = nums.length;
11  
12    // Populate the elements array with all values and their corresponding list indices
13    let index = 0;
14    for (let listIdx = 0; listIdx < numLists; listIdx++) {
15        for (const value of nums[listIdx]) {
16            elements[index++] = [value, listIdx];
17        }
18    }
19  
20    // Sort elements by value (ascending order)
21    elements.sort((a, b) => a[0] - b[0]);
22  
23    // Sliding window approach to find the smallest range
24    let left = 0;  // Left pointer of the window
25    const listCount = new Map<number, number>();  // Count of elements from each list in current window
26    let result: number[] = [-1000000, 1000000];  // Initialize with maximum possible range
27  
28    // Expand window with right pointer
29    for (let right = 0; right < totalElements; right++) {
30        const rightValue = elements[right][0];
31        const rightListIdx = elements[right][1];
32      
33        // Add current element's list to the window
34        listCount.set(rightListIdx, (listCount.get(rightListIdx) || 0) + 1);
35      
36        // Shrink window from left while it contains at least one element from each list
37        while (listCount.size === numLists) {
38            const leftValue = elements[left][0];
39            const leftListIdx = elements[left][1];
40          
41            // Check if current range is smaller than the best found so far
42            const rangeDiff = (rightValue - leftValue) - (result[1] - result[0]);
43            if (rangeDiff < 0 || (rangeDiff === 0 && leftValue < result[0])) {
44                // Update result with the new smaller range
45                result[0] = leftValue;
46                result[1] = rightValue;
47            }
48          
49            // Remove leftmost element from window
50            const newCount = listCount.get(leftListIdx)! - 1;
51            if (newCount === 0) {
52                listCount.delete(leftListIdx);
53            } else {
54                listCount.set(leftListIdx, newCount);
55            }
56            left++;
57        }
58    }
59  
60    return result;
61}
62

Time and Space Complexity

Time Complexity: O(n × log n) where n is the total number of elements across all arrays in nums.

  • Creating list t by iterating through all elements: O(n)
  • Sorting list t: O(n × log n) - This is the dominant operation
  • The sliding window loop iterates through all elements once: O(n)
  • Inside the loop, Counter operations (cnt[v] += 1, cnt[w] -= 1, cnt.pop(w)) are O(1) on average
  • The while loop moves pointer j at most n times total across all iterations

Therefore, the overall time complexity is O(n × log n) due to the sorting step.

Space Complexity: O(n) where n is the total number of elements across all arrays.

  • List t stores tuples for all elements: O(n)
  • Counter cnt stores at most k entries (where k is the number of arrays), but k ≤ n: O(k)O(n)
  • Variable ans and other variables: O(1)

The total space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Range Comparison Logic

One of the most common mistakes is incorrectly implementing the range comparison logic when updating the minimum range. Developers often forget to handle the tie-breaking condition properly.

Incorrect Implementation:

# Wrong: Only comparing range width
if right_value - left_value < min_range[1] - min_range[0]:
    min_range = [left_value, right_value]

Why it's wrong: This only updates when the new range is strictly smaller in width, but according to the problem, when two ranges have the same width, we should choose the one with the smaller starting point.

Correct Implementation:

current_range_diff = right_value - left_value - (min_range[1] - min_range[0])
if current_range_diff < 0 or (current_range_diff == 0 and left_value < min_range[0]):
    min_range = [left_value, right_value]

2. Premature Window Shrinking

Another pitfall is shrinking the window too aggressively or incorrectly managing when to stop shrinking.

Incorrect Implementation:

# Wrong: Shrinking without checking if all lists are still covered
while left < right:  # Should be: while len(list_counter) == len(nums)
    # shrink window logic

Why it's wrong: The window should only be shrunk while we still have at least one element from each list. Once we lose coverage of any list, we must stop shrinking and continue expanding.

3. Not Handling Counter Cleanup

Forgetting to remove lists from the counter when their count reaches zero can lead to incorrect window validity checks.

Incorrect Implementation:

# Wrong: Just decrementing without removing empty entries
list_counter[left_list_index] -= 1
# Missing: list_counter.pop(left_list_index) when count is 0

Why it's wrong: If we don't remove lists with zero count from the counter, len(list_counter) will give incorrect results, making us think we still have elements from all lists when we don't.

Correct Implementation:

list_counter[left_list_index] -= 1
if list_counter[left_list_index] == 0:
    list_counter.pop(left_list_index)

4. Using Wrong Initial Values

Initializing the answer range incorrectly can cause issues with the first valid range found.

Incorrect Implementation:

# Wrong: Using arbitrary values
min_range = [0, 0]  # or min_range = []

Why it's wrong: Using [0, 0] might accidentally be better than actual valid ranges, and using an empty list will cause comparison errors.

Correct Implementation:

min_range = [-inf, inf]  # Guarantees any valid range will be better

This ensures that the first valid range found will always replace the initial value since any finite range will be smaller than an infinite one.

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

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?


Recommended Readings

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

Load More