diff --git a/DIRECTORY.md b/DIRECTORY.md index 5229a424..7e29fe36 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -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 diff --git a/algorithms/intervals/remove_intervals/README.md b/algorithms/intervals/remove_intervals/README.md new file mode 100644 index 00000000..db18d4a1 --- /dev/null +++ b/algorithms/intervals/remove_intervals/README.md @@ -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 + +![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.. +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). diff --git a/algorithms/intervals/remove_intervals/__init__.py b/algorithms/intervals/remove_intervals/__init__.py new file mode 100644 index 00000000..22f71975 --- /dev/null +++ b/algorithms/intervals/remove_intervals/__init__.py @@ -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 diff --git a/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_1.png b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_1.png new file mode 100644 index 00000000..1ac2260a Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_1.png differ diff --git a/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_2.png b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_2.png new file mode 100644 index 00000000..f25819f9 Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_2.png differ diff --git a/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_3.png b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_3.png new file mode 100644 index 00000000..77f7732a Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_3.png differ diff --git a/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_4.png b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_4.png new file mode 100644 index 00000000..35cd0b80 Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_4.png differ diff --git a/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_5.png b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_5.png new file mode 100644 index 00000000..5f82fabd Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/examples/remove_covered_intervals_example_5.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_1.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_1.png new file mode 100644 index 00000000..1da40a9e Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_1.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_2.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_2.png new file mode 100644 index 00000000..80ba8843 Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_2.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_3.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_3.png new file mode 100644 index 00000000..cd6026fc Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_3.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_4.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_4.png new file mode 100644 index 00000000..10cfdcaf Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_4.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_5.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_5.png new file mode 100644 index 00000000..c4d99777 Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_5.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_6.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_6.png new file mode 100644 index 00000000..ed8ff32a Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_6.png differ diff --git a/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_7.png b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_7.png new file mode 100644 index 00000000..1a4a7d6c Binary files /dev/null and b/algorithms/intervals/remove_intervals/images/solutions/remove_covered_intervals_solution_7.png differ diff --git a/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py b/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py new file mode 100644 index 00000000..a2aa4409 --- /dev/null +++ b/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py @@ -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()