-
Notifications
You must be signed in to change notification settings - Fork 2
feat(algorithms, intervals): remove covered intervals #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| ## Examples | ||
|
|
||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|
|
||
|
|
||
| ## 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.. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation inconsistency with implementation. The README states "Initialize prev_end with zero" but the implementation initializes 🔎 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 |
||
| 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: | ||
|
|
||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|
|
||
| ### 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). | ||
| 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 |
| 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor typo in constraints.
"give" should be "given".
🔎 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents