LeetCode 2655 - Find Maximal Uncovered Ranges
The problem gives us an integer n, representing a conceptual array nums of length n, indexed from 0 to n - 1. We are also given a list of inclusive ranges, where each range marks positions in the array as covered.
Difficulty: 🟡 Medium
Topics: Array, Sorting
Solution
LeetCode 2655 - Find Maximal Uncovered Ranges
Problem Understanding
The problem gives us an integer n, representing a conceptual array nums of length n, indexed from 0 to n - 1. We are also given a list of inclusive ranges, where each range marks positions in the array as covered.
Our task is to determine every maximal uncovered segment of the array.
An uncovered range is a contiguous block of indices that are not included in any of the given covered ranges. The term maximal is important here. It means that if two uncovered intervals are adjacent, they must be merged into a single larger uncovered interval. In other words, the final answer should contain the largest possible contiguous uncovered segments.
For example, if n = 10 and the covered ranges are [3,5] and [7,8], then the covered indices are:
3,4,57,8
The uncovered indices are:
0,1,269
Since these uncovered groups are separated by covered cells, they remain separate maximal uncovered ranges:
[[0,2],[6,6],[9,9]]
The constraints are very important:
ncan be as large as10^9ranges.lengthcan be as large as10^6
These limits immediately tell us that we cannot explicitly build the entire array. Any solution that tries to create a boolean array of size n would be infeasible in both time and memory.
Instead, the solution must operate directly on the intervals themselves.
There are several important edge cases to recognize early:
- The ranges may overlap heavily.
- The ranges may already cover the entire array.
- The ranges list may be empty, meaning the entire array is uncovered.
- Adjacent covered intervals should effectively behave like one merged interval because they leave no uncovered space between them.
- The uncovered interval may appear before the first covered range or after the last covered range.
The problem guarantees that all ranges are valid and within bounds, so we do not need additional validation logic.
Approaches
Brute Force Approach
A straightforward solution would be to simulate the entire array.
We could create a boolean array of length n, initially marking every position as uncovered. Then, for every range [l, r], we mark all indices from l through r as covered.
After processing all ranges, we scan the array to collect maximal contiguous uncovered segments.
This works correctly because every position is explicitly tracked. However, the complexity is unacceptable for the given constraints.
Since n can be up to 10^9, constructing such an array is impossible in practice.
Optimal Approach
The key observation is that we never actually need to represent individual cells unless they are boundaries of intervals.
Instead, we can:
- Sort all ranges by starting position.
- Merge overlapping or adjacent covered ranges.
- Track the gaps between merged covered intervals.
Those gaps are exactly the maximal uncovered ranges.
For example:
ranges = [[2,4],[0,3]]
After sorting:
[[0,3],[2,4]]
After merging:
[[0,4]]
Now we can easily see that the uncovered portion is:
[5,6]
This interval-based approach is efficient because it processes only the given ranges rather than the entire conceptual array.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n + total covered cells) | O(n) | Explicitly marks every array position |
| Optimal | O(m log m) | O(m) | Sorts and merges intervals, where m is the number of ranges |
Here, m = ranges.length.
Algorithm Walkthrough
- First, sort the ranges by their starting coordinate.
Sorting allows us to process intervals from left to right and efficiently merge overlaps.
2. Initialize an empty result list to store uncovered ranges.
3. Maintain a variable called current_end.
This variable represents the furthest covered position processed so far.
Initially:
current_end = -1
This means nothing has been covered yet. 4. Iterate through the sorted ranges one by one.
For each interval [start, end], compare it against current_end.
5. If start > current_end + 1, then there is a gap between the previous covered region and the current interval.
The uncovered interval is:
[current_end + 1, start - 1]
Add this interval to the answer.
6. Update current_end.
Since intervals may overlap, we extend the covered region using:
current_end = max(current_end, end)
- After processing all ranges, there may still be an uncovered suffix at the end of the array.
If:
current_end < n - 1
then the remaining uncovered interval is:
[current_end + 1, n - 1]
- Return the collected uncovered intervals.
Why it works
The algorithm maintains the invariant that current_end always represents the furthest covered index among all processed intervals.
Because the ranges are processed in sorted order, any gap between current_end and the next interval's start must represent a maximal uncovered segment.
Merging overlaps ensures that no covered region is counted twice and no artificial gaps are introduced.
Therefore, every uncovered interval is found exactly once, and all returned intervals are maximal.
Python Solution
from typing import List
class Solution:
def findMaximalUncoveredRanges(
self,
n: int,
ranges: List[List[int]]
) -> List[List[int]]:
if not ranges:
return [[0, n - 1]]
ranges.sort()
uncovered = []
current_end = -1
for start, end in ranges:
if start > current_end + 1:
uncovered.append([current_end + 1, start - 1])
current_end = max(current_end, end)
if current_end < n - 1:
uncovered.append([current_end + 1, n - 1])
return uncovered
The implementation begins by handling the simplest edge case. If there are no covered ranges, then the entire array is uncovered, so we immediately return [0, n - 1].
Next, the ranges are sorted. Python's default list sorting naturally sorts by the first element and then the second element, which is exactly what we need.
The variable current_end tracks the furthest covered position encountered so far.
During iteration, if the next interval starts strictly after current_end + 1, then there is a genuine uncovered gap. That gap is appended to the answer list.
Afterward, we update current_end using the maximum value to merge overlapping intervals naturally.
Finally, after processing all ranges, we check whether any uncovered suffix remains at the end of the array.
Go Solution
package main
import "sort"
func findMaximalUncoveredRanges(n int, ranges [][]int) [][]int {
if len(ranges) == 0 {
return [][]int{{0, n - 1}}
}
sort.Slice(ranges, func(i, j int) bool {
if ranges[i][0] == ranges[j][0] {
return ranges[i][1] < ranges[j][1]
}
return ranges[i][0] < ranges[j][0]
})
result := [][]int{}
currentEnd := -1
for _, interval := range ranges {
start := interval[0]
end := interval[1]
if start > currentEnd+1 {
result = append(result, []int{currentEnd + 1, start - 1})
}
if end > currentEnd {
currentEnd = end
}
}
if currentEnd < n-1 {
result = append(result, []int{currentEnd + 1, n - 1})
}
return result
}
The Go implementation follows the same logic as the Python solution.
The main difference is that Go requires explicit sorting logic using sort.Slice. We also explicitly manage slice appends when building the result.
There are no integer overflow concerns because the constraints fit safely within Go's standard integer range.
Go slices naturally handle dynamic growth for the result intervals.
Worked Examples
Example 1
Input:
n = 10
ranges = [[3,5],[7,8]]
Step 1: Sort ranges
[[3,5],[7,8]]
Already sorted.
Step 2: Initialize variables
| Variable | Value |
|---|---|
| current_end | -1 |
| uncovered | [] |
Step 3: Process interval [3,5]
Check:
3 > -1 + 1
3 > 0
True, so uncovered range:
[0,2]
Update state:
| Variable | Value |
|---|---|
| uncovered | [[0,2]] |
| current_end | 5 |
Step 4: Process interval [7,8]
Check:
7 > 5 + 1
7 > 6
True, so uncovered range:
[6,6]
Update state:
| Variable | Value |
|---|---|
| uncovered | [[0,2],[6,6]] |
| current_end | 8 |
Step 5: Final suffix check
8 < 9
True, so append:
[9,9]
Final answer:
[[0,2],[6,6],[9,9]]
Example 2
Input:
n = 3
ranges = [[0,2]]
Process interval
| Variable | Value |
|---|---|
| current_end | 2 |
| uncovered | [] |
Final suffix check:
2 < 2
False.
Final answer:
[]
Example 3
Input:
n = 7
ranges = [[2,4],[0,3]]
Step 1: Sort ranges
[[0,3],[2,4]]
Step 2: Process [0,3]
No gap exists because:
0 > 0
False.
Update:
| Variable | Value |
|---|---|
| current_end | 3 |
Step 3: Process [2,4]
Check:
2 > 4
False.
Merge overlap:
| Variable | Value |
|---|---|
| current_end | 4 |
Step 4: Final suffix
4 < 6
True.
Append:
[5,6]
Final answer:
[[5,6]]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m log m) | Sorting the ranges dominates the runtime |
| Space | O(m) | Output storage and sorting overhead |
The algorithm processes each interval exactly once after sorting. The sorting step is the dominant cost.
No auxiliary structure proportional to n is created, which is crucial because n may be extremely large.
Test Cases
from typing import List
class Solution:
def findMaximalUncoveredRanges(
self,
n: int,
ranges: List[List[int]]
) -> List[List[int]]:
if not ranges:
return [[0, n - 1]]
ranges.sort()
uncovered = []
current_end = -1
for start, end in ranges:
if start > current_end + 1:
uncovered.append([current_end + 1, start - 1])
current_end = max(current_end, end)
if current_end < n - 1:
uncovered.append([current_end + 1, n - 1])
return uncovered
sol = Solution()
assert sol.findMaximalUncoveredRanges(
10,
[[3,5],[7,8]]
) == [[0,2],[6,6],[9,9]] # provided example
assert sol.findMaximalUncoveredRanges(
3,
[[0,2]]
) == [] # entire array covered
assert sol.findMaximalUncoveredRanges(
7,
[[2,4],[0,3]]
) == [[5,6]] # overlapping ranges
assert sol.findMaximalUncoveredRanges(
5,
[]
) == [[0,4]] # no ranges at all
assert sol.findMaximalUncoveredRanges(
8,
[[1,2],[4,5]]
) == [[0,0],[3,3],[6,7]] # multiple separate gaps
assert sol.findMaximalUncoveredRanges(
6,
[[0,1],[2,3],[4,5]]
) == [] # adjacent intervals cover everything
assert sol.findMaximalUncoveredRanges(
10,
[[1,7]]
) == [[0,0],[8,9]] # uncovered prefix and suffix
assert sol.findMaximalUncoveredRanges(
10,
[[1,3],[2,5],[4,8]]
) == [[0,0],[9,9]] # chained overlap merging
assert sol.findMaximalUncoveredRanges(
1,
[]
) == [[0,0]] # smallest possible n
assert sol.findMaximalUncoveredRanges(
1,
[[0,0]]
) == [] # smallest fully covered array
| Test | Why |
|---|---|
[[3,5],[7,8]] |
Validates standard gap detection |
[[0,2]] |
Ensures fully covered arrays return empty |
[[2,4],[0,3]] |
Validates overlap merging |
[] |
Ensures entire array is uncovered |
[[1,2],[4,5]] |
Tests multiple uncovered gaps |
[[0,1],[2,3],[4,5]] |
Tests adjacent intervals with no gaps |
[[1,7]] |
Tests uncovered prefix and suffix |
[[1,3],[2,5],[4,8]] |
Tests chained interval merging |
n = 1, [] |
Smallest boundary case |
n = 1, [[0,0]] |
Smallest fully covered case |
Edge Cases
One important edge case occurs when the ranges list is empty. In this scenario, no positions are covered, so the entire array from 0 to n - 1 must be returned as a single maximal uncovered interval. A naive implementation might accidentally return an empty list because it never enters the processing loop. The implementation handles this explicitly at the beginning.
Another important case involves overlapping intervals such as [[1,4],[2,6]]. If overlaps are not merged correctly, the algorithm could incorrectly identify fake uncovered gaps between intervals. The solution avoids this by maintaining current_end as the furthest covered position seen so far.
Adjacent intervals are another subtle case. For example:
[[0,2],[3,5]]
Although these intervals do not overlap, they leave no uncovered cells between them. A buggy implementation might incorrectly create an uncovered interval between 2 and 3. The condition:
start > current_end + 1
correctly prevents this issue because adjacency does not produce a gap.
Finally, uncovered regions at the beginning or end of the array can easily be missed. The implementation handles the beginning naturally through the initial value current_end = -1, and handles the suffix through the final post-processing check after all intervals are processed.