Taro Logo

Remove Covered Intervals

Medium
Asked by:
Profile picture
Profile picture
35 views
Topics:
ArraysGreedy Algorithms

Given an array intervals where intervals[i] = [li, ri] represent the interval [li, ri), remove all intervals that are covered by another interval in the list.

The interval [a, b) is covered by the interval [c, d) if and only if c <= a and b <= d.

Return the number of remaining intervals.

Example 1:

Input: intervals = [[1,4],[3,6],[2,8]]
Output: 2
Explanation: Interval [3,6] is covered by [2,8], therefore it is removed.

Example 2:

Input: intervals = [[1,4],[2,3]]
Output: 1

Constraints:

  • 1 <= intervals.length <= 1000
  • intervals[i].length == 2
  • 0 <= li < ri <= 105
  • All the given intervals are unique.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What is the maximum size of the input array of intervals?
  2. Can the start and end values of the intervals be negative?
  3. Are intervals guaranteed to be valid, i.e., will start always be less than or equal to end?
  4. If multiple intervals are completely covered by the same single interval, should I only count the covering interval once?
  5. If two intervals are identical (start and end are the same), should I consider one as covering the other, and which one should be removed?

Brute Force Solution

Approach

The brute force method for this interval problem is straightforward: we will check every single possible pair of intervals. We want to see if one interval completely contains another.

Here's how the algorithm would work step-by-step:

  1. Take the first interval from the list.
  2. Compare this interval to every other interval in the list.
  3. If the first interval completely covers another interval, mark the covered interval as one we can remove.
  4. Repeat this process, taking the second interval and comparing it against all the other intervals. Again, mark any covered intervals.
  5. Continue this process until every interval has been compared to every other interval.
  6. After comparing every interval against all other intervals, count the number of intervals that were NOT marked as covered. This count is the number of intervals that remain.

Code Implementation

def remove_covered_intervals_brute_force(intervals):
    number_of_intervals = len(intervals)
    covered_intervals = [False] * number_of_intervals

    # Iterate through each interval
    for first_interval_index in range(number_of_intervals):
        for second_interval_index in range(number_of_intervals):
            # Avoid comparing an interval to itself.
            if first_interval_index == second_interval_index:
                continue

            #Check for coverage.
            if (intervals[first_interval_index][0] <= intervals[second_interval_index][0] and\
                intervals[first_interval_index][1] >= intervals[second_interval_index][1]):

                # If the second interval is covered by the first.
                covered_intervals[second_interval_index] = True

    # Count the number of intervals that are not covered.
    number_of_uncovered_intervals = 0
    for is_covered in covered_intervals:
        if not is_covered:
            number_of_uncovered_intervals += 1

    return number_of_uncovered_intervals

Big(O) Analysis

Time Complexity
O(n²)The provided brute force approach involves iterating through each of the n intervals in the input list. For each interval, we compare it against all other n-1 intervals to determine if any are covered. This results in a nested loop structure where the outer loop runs n times and the inner loop runs approximately n times. Thus, the total number of comparisons is proportional to n multiplied by n, leading to a time complexity of O(n²).
Space Complexity
O(N)The algorithm uses a data structure to mark covered intervals. Given N intervals, the algorithm marks each interval that is covered, requiring up to N boolean values (or similar mechanism) to track which intervals to remove. The space needed for this marking mechanism grows linearly with the number of intervals. Therefore, the auxiliary space complexity is O(N).

Optimal Solution

Approach

The goal is to find out how many intervals are NOT completely covered by others. The trick is to first organize the intervals in a way that makes checking for coverage easier, and then efficiently compare them to identify the ones that are covered.

Here's how the algorithm would work step-by-step:

  1. First, put the intervals in order. Sort them starting with the interval that begins earliest. If some intervals start at the same point, put the one that goes the furthest first.
  2. Now, go through the sorted list of intervals. Keep track of the furthest point any interval has reached so far.
  3. For each new interval, check if its end point is less than or equal to the furthest point we've seen. If it is, that means the current interval is covered by a previous interval, so we don't count it.
  4. If the current interval's end point goes further than the furthest point we've seen, then this interval is not covered. We count it, and update our furthest point to be the end of the current interval.
  5. In the end, the number of intervals we counted is the number of intervals that are not covered.

Code Implementation

def remove_covered_intervals(intervals):
    intervals.sort(key=lambda x: (x[0], -x[1]))

    number_of_uncovered_intervals = 0
    current_max_right = -1

    for interval_start, interval_end in intervals:
        # If current interval is covered by previous,
        # then we don't count it.
        if interval_end <= current_max_right:
            continue

        # Count the interval because it is not covered.
        number_of_uncovered_intervals += 1

        # Update the furthest right endpoint.
        current_max_right = interval_end

    return number_of_uncovered_intervals

Big(O) Analysis

Time Complexity
O(n log n)The most significant operation is sorting the input array of intervals, which takes O(n log n) time. The subsequent iteration through the sorted intervals involves a single loop to check for covered intervals. This loop runs in O(n) time. Since O(n log n) dominates O(n), the overall time complexity is O(n log n).
Space Complexity
O(1)The algorithm primarily uses a few variables to keep track of the furthest point reached so far and the count of non-covered intervals. No auxiliary data structures like lists or hash maps are created that scale with the number of intervals, N. Therefore, the space used remains constant irrespective of the input size. Consequently, the space complexity is O(1).

Edge Cases

Empty or null input array
How to Handle:
Return 0, as there are no intervals to begin with.
Input array with only one interval
How to Handle:
Return 1, since a single interval can't be covered.
All intervals are identical
How to Handle:
Return 1, as all intervals are covering each other, leaving only one uncovered.
Large input array causing potential performance issues
How to Handle:
Sorting the intervals by start time can improve performance, and efficient comparison logic avoids unnecessary iterations.
Intervals with identical start times but different end times
How to Handle:
Sort the intervals by start time, then by descending end time to correctly identify covering intervals.
Intervals with negative or zero values
How to Handle:
The comparison logic should handle negative and zero interval boundaries correctly.
Integer overflow if interval endpoints are very large
How to Handle:
Use appropriate data types (long or similar) or comparison methods to avoid potential integer overflow issues.
No intervals are covered by any other interval
How to Handle:
The algorithm should return the total number of intervals in the input.