2655. Find Maximal Uncovered Ranges π
Problem Description
You have an array nums
of length n
(0-indexed, meaning positions go from 0 to n-1), and you're given a list of ranges that cover some portions of this array. Each range is defined by two values: a start position (inclusive) and an end position (inclusive).
Your task is to find all the gaps (uncovered portions) in the array that are not covered by any of the given ranges.
Key requirements:
- Find all uncovered ranges that are maximal (meaning they should be as large as possible)
- Each uncovered cell should belong to exactly one uncovered range
- Adjacent uncovered ranges should be merged (there shouldn't be two ranges where one ends at position
r
and the next starts at positionr+1
) - Return the uncovered ranges sorted by their starting positions in ascending order
Example to illustrate:
If n = 10
(array positions 0-9) and you have ranges [[1,3], [5,7]]
:
- Position 0 is uncovered
- Positions 1-3 are covered by the first range
- Position 4 is uncovered
- Positions 5-7 are covered by the second range
- Positions 8-9 are uncovered
The uncovered ranges would be: [[0,0], [4,4], [8,9]]
The solution works by:
- Sorting the given ranges by their start positions
- Tracking the last covered position as we iterate through the sorted ranges
- Whenever there's a gap between the last covered position and the start of the next range, that gap is an uncovered range
- After processing all ranges, checking if there's an uncovered portion at the end of the array
Intuition
The key insight is to think about this problem as walking along a number line from position 0 to n-1, and identifying the gaps between covered regions.
Imagine you're painting segments on a wall. Some segments are already painted (the given ranges), and you need to find all the unpainted segments. The natural approach is to walk from left to right and note down each unpainted section you encounter.
To make this process efficient, we first sort the ranges by their starting positions. This allows us to process them in order from left to right, making it easy to identify gaps.
As we traverse through the sorted ranges, we maintain a variable last
that tracks the rightmost position we've covered so far. This is crucial because:
- If a new range starts at position
l
and ourlast
covered position is less thanl-1
, then positions[last+1, l-1]
form an uncovered gap - When ranges overlap, we need to update
last
to be the maximum of the currentlast
and the end of the current range (this handles overlapping ranges correctly)
The beauty of this approach is that by processing ranges in sorted order and keeping track of the furthest point reached, we automatically:
- Merge overlapping ranges (by taking the maximum endpoint)
- Identify all gaps between ranges
- Ensure gaps are maximal (we don't split them unnecessarily)
Finally, after processing all ranges, we check if there's a gap at the end of the array (from last+1
to n-1
), ensuring we don't miss any uncovered portion at the tail end.
This greedy approach works because once we've processed ranges up to a certain point, we know exactly what's covered and what's not, allowing us to make optimal local decisions that lead to the globally correct answer.
Learn more about Sorting patterns.
Solution Approach
The implementation follows a sweep line approach with greedy processing:
Step 1: Sort the ranges
ranges.sort()
We sort the ranges by their starting positions (Python's default sort for lists). This ensures we process ranges from left to right, making gap detection straightforward.
Step 2: Initialize tracking variables
last = -1 ans = []
last
tracks the rightmost covered position seen so far, initialized to-1
(one position before the array starts)ans
stores our result list of uncovered ranges
Step 3: Process each range and detect gaps
for l, r in ranges:
if last + 1 < l:
ans.append([last + 1, l - 1])
last = max(last, r)
For each range [l, r]
:
- Gap detection: If
last + 1 < l
, there's a gap between positionlast + 1
andl - 1
. This gap is uncovered, so we add[last + 1, l - 1]
to our answer. - Update coverage: We update
last = max(last, r)
to handle overlapping ranges. If the current range extends further right than our previous coverage, we extendlast
. If not (the range is completely contained in already covered area),last
remains unchanged.
Step 4: Check for trailing uncovered range
if last + 1 < n: ans.append([last + 1, n - 1])
After processing all ranges, if last + 1 < n
, there's an uncovered portion at the end of the array from position last + 1
to n - 1
.
Why this works:
- By processing ranges in sorted order, we ensure we never miss a gap
- The
max(last, r)
operation correctly handles overlapping ranges without double-counting - The gap detection condition
last + 1 < l
ensures we only add truly uncovered ranges (no adjacent ranges that should be merged) - Time complexity:
O(m log m)
where m is the number of ranges (dominated by sorting) - Space complexity:
O(1)
extra space (not counting the output array)
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a concrete example to see how the algorithm works step by step.
Given:
n = 12
(array positions 0-11)ranges = [[5,7], [2,3], [9,10]]
Step 1: Sort the ranges by start position
After sorting: ranges = [[2,3], [5,7], [9,10]]
Step 2: Initialize variables
last = -1
(tracking rightmost covered position)ans = []
(result list)
Step 3: Process each range
Processing range [2,3]:
- Check for gap:
last + 1 = 0
, range starts at2
- Since
0 < 2
, there's a gap from position 0 to 1 - Add
[0, 1]
to answer:ans = [[0,1]]
- Update
last = max(-1, 3) = 3
Processing range [5,7]:
- Check for gap:
last + 1 = 4
, range starts at5
- Since
4 < 5
, there's a gap at position 4 - Add
[4, 4]
to answer:ans = [[0,1], [4,4]]
- Update
last = max(3, 7) = 7
Processing range [9,10]:
- Check for gap:
last + 1 = 8
, range starts at9
- Since
8 < 9
, there's a gap at position 8 - Add
[8, 8]
to answer:ans = [[0,1], [4,4], [8,8]]
- Update
last = max(7, 10) = 10
Step 4: Check for trailing uncovered portion
last + 1 = 11
,n = 12
- Since
11 < 12
, there's an uncovered portion at position 11 - Add
[11, 11]
to answer:ans = [[0,1], [4,4], [8,8], [11,11]]
Final Result: [[0,1], [4,4], [8,8], [11,11]]
This represents the uncovered portions of the array:
- Positions 0-1 are uncovered
- Positions 2-3 are covered by range [2,3]
- Position 4 is uncovered
- Positions 5-7 are covered by range [5,7]
- Position 8 is uncovered
- Positions 9-10 are covered by range [9,10]
- Position 11 is uncovered
Solution Implementation
1class Solution:
2 def findMaximalUncoveredRanges(
3 self, n: int, ranges: List[List[int]]
4 ) -> List[List[int]]:
5 """
6 Find all maximal uncovered ranges in [0, n-1] that are not covered by the given ranges.
7
8 Args:
9 n: The upper bound (exclusive) of the range [0, n-1]
10 ranges: List of ranges [left, right] that cover certain intervals
11
12 Returns:
13 List of maximal uncovered ranges [left, right]
14 """
15 # Sort ranges by their starting positions for sequential processing
16 ranges.sort()
17
18 # Track the rightmost position covered so far
19 last_covered = -1
20
21 # Store all uncovered ranges
22 uncovered_ranges = []
23
24 # Process each range in sorted order
25 for left, right in ranges:
26 # Check if there's a gap between last covered position and current range start
27 if last_covered + 1 < left:
28 # Add the uncovered range between last_covered and current range
29 uncovered_ranges.append([last_covered + 1, left - 1])
30
31 # Update the rightmost covered position
32 # Use max to handle overlapping ranges
33 last_covered = max(last_covered, right)
34
35 # Check if there's an uncovered range at the end
36 if last_covered + 1 < n:
37 uncovered_ranges.append([last_covered + 1, n - 1])
38
39 return uncovered_ranges
40
1class Solution {
2 /**
3 * Finds all maximal uncovered ranges in [0, n-1] that are not covered by the given ranges.
4 *
5 * @param n The upper bound (exclusive) of the overall range [0, n-1]
6 * @param ranges Array of ranges that cover portions of [0, n-1]
7 * @return Array of maximal uncovered ranges
8 */
9 public int[][] findMaximalUncoveredRanges(int n, int[][] ranges) {
10 // Sort ranges by their starting positions in ascending order
11 Arrays.sort(ranges, (a, b) -> a[0] - b[0]);
12
13 // Track the rightmost position covered so far
14 int lastCoveredPosition = -1;
15
16 // List to store the uncovered ranges
17 List<int[]> uncoveredRanges = new ArrayList<>();
18
19 // Process each range in sorted order
20 for (int[] range : ranges) {
21 int rangeStart = range[0];
22 int rangeEnd = range[1];
23
24 // Check if there's a gap between the last covered position and current range start
25 if (lastCoveredPosition + 1 < rangeStart) {
26 // Add the uncovered range between last covered position and current range
27 uncoveredRanges.add(new int[] {lastCoveredPosition + 1, rangeStart - 1});
28 }
29
30 // Update the rightmost covered position
31 lastCoveredPosition = Math.max(lastCoveredPosition, rangeEnd);
32 }
33
34 // Check if there's an uncovered range at the end (after all ranges)
35 if (lastCoveredPosition + 1 < n) {
36 uncoveredRanges.add(new int[] {lastCoveredPosition + 1, n - 1});
37 }
38
39 // Convert the list to a 2D array and return
40 return uncoveredRanges.toArray(new int[0][]);
41 }
42}
43
1class Solution {
2public:
3 vector<vector<int>> findMaximalUncoveredRanges(int n, vector<vector<int>>& ranges) {
4 // Sort ranges by their starting positions in ascending order
5 sort(ranges.begin(), ranges.end(), [](const vector<int>& a, const vector<int>& b) {
6 return a[0] < b[0];
7 });
8
9 // Track the rightmost covered position
10 int lastCoveredPosition = -1;
11
12 // Store the result of uncovered ranges
13 vector<vector<int>> uncoveredRanges;
14
15 // Iterate through each sorted range
16 for (const auto& range : ranges) {
17 int rangeStart = range[0];
18 int rangeEnd = range[1];
19
20 // If there's a gap between the last covered position and current range start,
21 // add the uncovered interval to the result
22 if (lastCoveredPosition + 1 < rangeStart) {
23 uncoveredRanges.push_back({lastCoveredPosition + 1, rangeStart - 1});
24 }
25
26 // Update the rightmost covered position
27 lastCoveredPosition = max(lastCoveredPosition, rangeEnd);
28 }
29
30 // Check if there's an uncovered range at the end
31 if (lastCoveredPosition + 1 < n) {
32 uncoveredRanges.push_back({lastCoveredPosition + 1, n - 1});
33 }
34
35 return uncoveredRanges;
36 }
37};
38
1function findMaximalUncoveredRanges(n: number, ranges: number[][]): number[][] {
2 // Sort ranges by their starting positions in ascending order
3 ranges.sort((a: number[], b: number[]) => a[0] - b[0]);
4
5 // Track the rightmost covered position
6 let lastCoveredPosition: number = -1;
7
8 // Store the result of uncovered ranges
9 const uncoveredRanges: number[][] = [];
10
11 // Iterate through each sorted range
12 for (const range of ranges) {
13 const rangeStart: number = range[0];
14 const rangeEnd: number = range[1];
15
16 // If there's a gap between the last covered position and current range start,
17 // add the uncovered interval to the result
18 if (lastCoveredPosition + 1 < rangeStart) {
19 uncoveredRanges.push([lastCoveredPosition + 1, rangeStart - 1]);
20 }
21
22 // Update the rightmost covered position
23 lastCoveredPosition = Math.max(lastCoveredPosition, rangeEnd);
24 }
25
26 // Check if there's an uncovered range at the end
27 if (lastCoveredPosition + 1 < n) {
28 uncoveredRanges.push([lastCoveredPosition + 1, n - 1]);
29 }
30
31 return uncoveredRanges;
32}
33
Time and Space Complexity
Time Complexity: O(n log n)
where n
is the length of the ranges list.
- The sorting operation takes
O(n log n)
time - The subsequent iteration through the sorted ranges takes
O(n)
time - Each operation inside the loop (comparison, append, max) takes
O(1)
time - Overall time complexity is dominated by the sorting step:
O(n log n)
Space Complexity: O(n)
in the worst case.
- The sorting operation may use
O(log n)
toO(n)
auxiliary space depending on the implementation (Python's Timsort usesO(n)
in worst case) - The output list
ans
can contain at mostn+1
intervals in the worst case (when no ranges overlap and create maximum gaps) - Therefore, the total space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling Overlapping Ranges Correctly
A critical pitfall occurs when developers forget to use max(last, r)
and instead simply set last = r
. This breaks when ranges overlap or when a range is completely contained within previously covered area.
Incorrect Code:
for l, r in ranges: if last + 1 < l: ans.append([last + 1, l - 1]) last = r # WRONG! Should be max(last, r)
Problem Example:
- Input:
n = 10
,ranges = [[1, 5], [2, 3]]
- After processing
[1, 5]
:last = 5
- After processing
[2, 3]
:last = 3
(incorrectly moves backward!) - This would incorrectly report
[4, 4]
as uncovered when it's actually covered
Solution:
Always use last = max(last, r)
to ensure we never move the coverage boundary backward.
2. Incorrect Initial Value for last
Setting last = 0
instead of last = -1
causes the algorithm to miss uncovered ranges at the beginning.
Incorrect Code:
last = 0 # WRONG! Should be -1 ans = []
Problem Example:
- Input:
n = 5
,ranges = [[2, 4]]
- With
last = 0
, the conditionlast + 1 < 2
gives1 < 2
, missing position 0 - Would incorrectly output
[[1, 1]]
instead of[[0, 1]]
Solution:
Initialize last = -1
to represent "nothing covered yet" and ensure position 0 can be properly detected as uncovered.
3. Off-by-One Errors in Gap Detection
Developers often make mistakes with the boundary calculations when detecting gaps.
Common Mistakes:
# Mistake 1: Wrong gap boundaries if last < l: # Should be last + 1 < l ans.append([last, l]) # Should be [last + 1, l - 1] # Mistake 2: Including already covered positions if last + 1 <= l: # Should use < not <= ans.append([last + 1, l - 1])
Problem Example:
- Input:
n = 10
,ranges = [[1, 3], [4, 6]]
- With
last + 1 <= l
, after processing[1, 3]
withlast = 3
, we'd check4 <= 4
(true) - This would add
[4, 3]
(invalid range) to the answer
Solution:
Use strict inequality last + 1 < l
and correct boundaries [last + 1, l - 1]
for the uncovered range.
4. Forgetting to Check the End of the Array
A common oversight is processing all ranges but forgetting to check if there's an uncovered portion after the last range.
Incomplete Code:
for l, r in ranges:
if last + 1 < l:
ans.append([last + 1, l - 1])
last = max(last, r)
# Missing the final check!
return ans
Problem Example:
- Input:
n = 10
,ranges = [[0, 5]]
- Without the final check, positions 6-9 would not be reported as uncovered
Solution: Always add the final check after the loop:
if last + 1 < n: ans.append([last + 1, n - 1])
5. Not Sorting the Ranges First
Processing ranges in arbitrary order makes gap detection impossible or extremely complex.
Problem Example:
- Input:
n = 10
,ranges = [[5, 7], [1, 3]]
- Processing
[5, 7]
first would incorrectly identify[0, 4]
as uncovered - Then processing
[1, 3]
wouldn't fix the mistake
Solution:
Always sort ranges by starting position before processing: ranges.sort()
Which of the following is a good use case for backtracking?
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!