Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
* [Test Min Meeting Rooms](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/meeting_rooms/test_min_meeting_rooms.py)
* Merge Intervals
* [Test Merge Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/merge_intervals/test_merge_intervals.py)
* Remove Intervals
* [Test Remove Covered Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py)
* Task Scheduler
* [Test Task Scheduler](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/task_scheduler/test_task_scheduler.py)
* Josephus Circle
Expand Down
72 changes: 72 additions & 0 deletions algorithms/intervals/remove_intervals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Remove Covered Intervals

Given an array of intervals, where each interval is represented as intervals[i]=[li,ri) (indicating the range from
li to ri, inclusive of li and exclusive of ri), remove all intervals that are completely covered by another interval in
the list. Return the count of intervals that remain after removing the covered ones.

> Note An interval [a, b) is considered covered by another interval [c,d) if and only if c ⇐ a and b ⇐ d.

## Constraints

- 1 <= intervals.length <= 10^4
- intervals[i].length == 2
- 0 <= li < ri <= 10^5
- All the give intervals are unique
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor typo in constraints.

"give" should be "given".

🔎 Suggested fix
-- All the give intervals are unique
+- All the given intervals are unique
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- All the give intervals are unique
- All the given intervals are unique
🤖 Prompt for AI Agents
In algorithms/intervals/remove_intervals/README.md around line 14, there's a
typo in the constraints: replace "All the give intervals are unique" with "All
the given intervals are unique" to correct the grammar; update the sentence
accordingly and save the README.


## Examples

![Example 1](./images/examples/remove_covered_intervals_example_1.png)
![Example 2](./images/examples/remove_covered_intervals_example_2.png)
![Example 3](./images/examples/remove_covered_intervals_example_3.png)
![Example 4](./images/examples/remove_covered_intervals_example_4.png)
![Example 5](./images/examples/remove_covered_intervals_example_5.png)


## Solution

The first step is to simplify the process by sorting the intervals. Sorting by the start point in ascending order is
straightforward and simplifies the iteration process. However, an important edge case arises when two intervals share
the same start point. In such scenarios, sorting solely by the start point would fail to correctly identify covered
intervals. To handle this, we sort intervals with the same start point by their endpoint in descending order, ensuring
that longer intervals come first. This sorting strategy guarantees that if one interval covers another, it will be
positioned earlier in the sorted list. Once the intervals are sorted, we iterate through them while keeping track of the
maximum endpoint seen so far. If the current interval’s end point exceeds this maximum, it is not covered, so we increment
the count and update the maximum end. The interval is covered and skipped if the endpoint is less than or equal to the
maximum. After completing the iteration, the final count reflects the remaining non-covered intervals.

Now, let’s look at the solution steps below:

1. If the start points are the same, sort the intervals by the start point in ascending order, otherwise, sort by the
endpoint in descending order to prioritize longer intervals.
2. Initialize the count with zero to track the remaining (non-covered) intervals.
3. Initialize prev_end with zero to track the maximum end value we’ve seen..
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation inconsistency with implementation.

The README states "Initialize prev_end with zero" but the implementation initializes max_end_seen with float('-inf'). While both work correctly for this problem (since intervals have non-negative bounds per constraints), the documentation should match the actual implementation for clarity.

🔎 Suggested fix
-3. Initialize prev_end with zero to track the maximum end value we've seen.. 
+3. Initialize prev_end with negative infinity to track the maximum end value we've seen. 
🤖 Prompt for AI Agents
In algorithms/intervals/remove_intervals/README.md around line 42, the README
says "Initialize prev_end with zero" but the implementation initializes
max_end_seen with float('-inf'); update the README to match the implementation
by changing the documentation text to say "Initialize max_end_seen with
float('-inf')" (or otherwise mention that the code uses negative infinity) so
docs and code agree, or alternatively change the implementation to initialize
with 0 if you prefer that convention — ensure both places use the same variable
name and value.

4. Start iterating through intervals for each interval [start, end] in the sorted list:
- If end > prev_end, any previous interval does not cover the interval.
- Increment count by 1
- Update prev_end to end.
- Else:
- A previous interval covers the interval, so we skip it.

5. After iterating through all the intervals, the return count is the final value, representing the remaining intervals.

Let’s look at the following illustration to get a better understanding of the solution:

![Solution 1](./images/solutions/remove_covered_intervals_solution_1.png)
![Solution 2](./images/solutions/remove_covered_intervals_solution_2.png)
![Solution 3](./images/solutions/remove_covered_intervals_solution_3.png)
![Solution 4](./images/solutions/remove_covered_intervals_solution_4.png)
![Solution 5](./images/solutions/remove_covered_intervals_solution_5.png)
![Solution 6](./images/solutions/remove_covered_intervals_solution_6.png)
![Solution 7](./images/solutions/remove_covered_intervals_solution_7.png)

### Time Complexity

The time complexity of the solution is O(n logn), where n is the number of intervals. This is because sorting the
intervals takes O(n logn) time, and the subsequent iteration through the intervals takes O(n) time. Therefore, the
overall time complexity is dominated by the sorting step, resulting in O(n logn).

### Space Complexity

The sorting operation has a space complexity of O(n) in the worst case due to additional memory required for temporary
arrays during the sorting process. Apart from the space used by the built-in sorting algorithm, the algorithm’s space
complexity is constant, O(1). Therefore, the overall space complexity of the solution is O(n).
43 changes: 43 additions & 0 deletions algorithms/intervals/remove_intervals/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import List


def remove_covered_intervals(intervals: List[List[int]]) -> int:
"""
Finds the number of intervals that are not covered by any other interval in the list.

This uses a greedy approach to find the number of intervals that are not covered by any other interval in the list.
Note An interval [a, b) is considered covered by another interval [c,d) if and only if c ⇐ a and b ⇐ d.

So, the timeline will look something like this:
c---a---b---d

If b <= d, then [a, b) is covered by [c, d)

Args:
intervals (List[List[int]]): A list of intervals, where each interval is represented as [start, end]
Returns:
int: The number of intervals that are not covered by any other interval in the list
"""
# early return if there are no intervals to begin with
if not intervals:
return 0

# Sort intervals by start time in ascending order and then by end time in descending order. We sort by the end time
# to remove a tie-breaker where two intervals have the same start time.
# This will incur a time complexity of O(nlogn). We sort in place, so, we do not
# incur any additional space complexity by copying over to a new list. The assumption made here is that it is okay
# to mutate the input list.
intervals.sort(key=lambda x: (x[0], -x[1]))

# keep track of the last max end seen so far, we use a large negative infinity to cover all possible numbers
max_end_seen = float('-inf')
count = 0

# We then iterate through the given intervals
for _, current_end in intervals:
if current_end > max_end_seen:
count += 1
max_end_seen = current_end

# return the count of non-overlapping intervals
return count
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import unittest
from typing import List
from copy import deepcopy
from parameterized import parameterized
from algorithms.intervals.remove_intervals import remove_covered_intervals

TEST_CASES = [
([[1, 5], [2, 5], [3, 5], [4, 5]], 1),
([[1, 3], [3, 6], [6, 9]], 3),
([[1, 3], [4, 6], [7, 9]], 3),
([[1, 10], [2, 9], [3, 8], [4, 7]], 1),
([[1, 4], [3, 6], [2, 8]], 2),
([[1, 2], [1, 4], [3, 4]], 1),
([[1, 5], [2, 3], [4, 6]], 2),
(
[
[33, 40],
[39, 48],
[39, 53],
[63, 68],
[46, 53],
[56, 62],
[71, 79],
[67, 73],
[103, 114],
[51, 58],
[47, 54],
[123, 130],
[142, 157],
[66, 71],
[151, 164],
[80, 85],
[94, 105],
[100, 107],
[164, 172],
[105, 119],
[221, 226],
[171, 176],
[98, 105],
[129, 137],
[272, 281],
[66, 76],
[38, 53],
[176, 185],
[264, 269],
[243, 255],
[100, 105],
[343, 357],
[192, 207],
[314, 325],
[77, 84],
[208, 222],
[54, 59],
[48, 59],
[138, 150],
[78, 88],
[33, 46],
[70, 84],
[35, 44],
[282, 295],
[128, 136],
[472, 485],
[177, 189],
[190, 196],
[398, 413],
[108, 121],
[375, 385],
[376, 388],
[422, 429],
[519, 527],
[46, 51],
[530, 543],
[345, 360],
[570, 575],
[374, 388],
[527, 534],
[271, 282],
[430, 436],
[81, 91],
[136, 149],
[494, 500],
[52, 60],
],
48,
),
]


class RemoveCoveredIntervalsTestCase(unittest.TestCase):
@parameterized.expand(TEST_CASES)
def test_remove_closed_intervals(self, intervals: List[List[int]], expected: int):
input_intervals = deepcopy(intervals)
actual = remove_covered_intervals(input_intervals)
self.assertEqual(expected, actual)


if __name__ == "__main__":
unittest.main()
Loading