2406. Divide Intervals Into Minimum Number of Groups


Problem Description

You are given a list of intervals, and each interval is a list of two integers indicating the start and the end of that interval. The goal is to divide these intervals into groups such that no two intervals in the same group overlap each other. An overlap means that there is at least one number that is contained in both intervals. For instance, [1, 5] and [5, 8] are considered overlapping because they both contain the number 5. You need to find the minimum number of groups necessary to achieve this separation.

Intuition

The key to solving this problem is understanding that whenever we have a new interval, it can either be added to an existing group if it doesn't overlap with other intervals in that group, or we need to create a new group if it overlaps with all existing groups. The problem resembles organizing meeting rooms: each interval is like a meeting, and each group is a meeting room. The challenge is to book the minimum number of meeting rooms.

To figure out the solution, we can:

  1. Sort the intervals based on the starting points. This way, we consider the intervals in the order of their starting times.

  2. Use a min-heap to keep track of the end times of the last interval in each group. A min-heap is a binary tree where the parent node is always less than or equal to its child nodes; therefore, the smallest element is always at the root of the tree, and we can access it in constant time.

  3. For each interval, check the root of the min-heap, which gives the earliest end time of all groups. If the start of the current interval is greater than the earliest end time, this means that we can add the interval to this group without overlaps, and thus, we replace the old end time with the end time of the current interval by popping the root of the heap and pushing the new end time.

  4. If the start of the interval is not greater than the earliest end time in the heap, it means that it overlaps with every group, and we need to start a new group. So, we push the end time of this interval into the heap.

  5. Continue this process until we have checked all intervals. The size of the heap at the end denotes the minimum number of groups needed, as each item in the heap represents a group's latest end time and all intervals within that group that do not overlap.

The given Python solution follows this approach, using the heapq module to manage the min-heap operations efficiently.

Learn more about Greedy, Two Pointers, Prefix Sum, Sorting and Heap (Priority Queue) patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece๏ผš

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?

Solution Approach

The solution uses a min-heap to efficiently manage the end points of the intervals in the current groups and the sorting pattern to preprocess the intervals. The steps for the algorithm are as follows:

  1. Sorting the intervals: The given intervals are sorted based on their starting point using sorted(intervals). Sorting is crucial because it allows us to go through intervals sequentially, considering the earliest starting interval first.

  2. Initializing the min-heap: A min-heap h is used to keep track of the end points of the intervals in the various groups. In Python, this is typically implemented using the heapq module.

  3. Iterating through the sorted intervals: For every interval (a, b) in sorted(intervals):

    • Check if there is an existing group that the interval can be added to without overlapping. This is done by examining the smallest end point in the min-heap h[0]. If h[0] < a, it indicates that there's a group whose last interval ends before the current interval starts, so they do not overlap.

    • If there's an overlap with all existing groups (h[0] >= a), the interval needs to be placed in a new group. Therefore, we push the end point b of the current interval onto the heap using heappush(h, b).

    • If there is no overlap (h[0] < a), we remove the end point of the finished group from the heap using heappop(h) and then push the end point b of the current interval. This effectively updates the group end point to the current interval's end point without increasing the number of groups.

  4. Returning the result: After all intervals have been processed, the number of elements in the min-heap corresponds to the number of groups needed. Each element in the heap represents the end time of a group that does not overlap with the others. The length of the heap len(h) is returned as the minimum number of groups required.

This algorithm ensures that all intervals are in exactly one group and that no two intervals in the same group intersect by always checking for the earliest possible group that an interval could belong to and creating a new group only when necessary.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Example Walkthrough

Let's take the following list of intervals as an example to walk through the solution approach:

1Intervals: [[1, 4], [2, 5], [7, 9], [8, 10], [6, 8]]

Steps:

  1. Sorting the intervals:

    • First, we sort the intervals based on their start times.
    • Sorted Intervals: [[1, 4], [2, 5], [6, 8], [7, 9], [8, 10]]
  2. Initializing the min-heap:

    • We initiate a min-heap to keep track of end times of the formed groups.
    • Initial heap h: []
  3. Iterating through the sorted intervals:

    • For the first interval [1, 4], the heap is empty, so we push the end time 4 onto the heap.

      • Heap h: [4]
    • Next, consider interval [2, 5]. Since it starts before interval [1, 4] ends (h[0] < 2 is false), we cannot add it to the same group. So, we push its end time 5 onto the heap.

      • Heap h: [4, 5]
    • Then, look at interval [6, 8]. Since it starts after interval [1, 4] ends (h[0] < 6 is true), we pop 4 and push end time 8.

      • Heap h: [5, 8]
    • The fourth interval [7, 9] starts before interval [5, 8] ends (h[0] < 7 is false), hence we need a new group. We push end time 9.

      • Heap h: [5, 8, 9]
    • Finally, we have interval [8, 10]. It starts after interval [5, 8] ends (h[0] < 8 is true), so we pop 5 and push end time 10.

      • Heap h: [8, 9, 10]
  4. Returning the result:

    • With all intervals processed, the size of the heap is 3, indicating that we need at least 3 groups to separate the intervals with no overlaps.
    • Hence, the minimum number of groups required is 3.

By adhering to this method, we ensure each interval is placed in the correct group without overlaps, and the number of groups is minimized.

Solution Implementation

1from typing import List
2import heapq
3
4class Solution:
5    def minGroups(self, intervals: List[List[int]]) -> int:
6        # Initialize a min-heap to store end times of intervals
7        min_heap = []
8
9        # Sort the intervals based on starting time
10        sorted_intervals = sorted(intervals, key=lambda x: x[0])
11
12        # Loop through each interval in the sorted list
13        for start, end in sorted_intervals:
14            # If the heap is not empty and the smallest end time is less than
15            # the current interval's start time, remove the interval from the heap
16            # since it doesn't overlap with the current one.
17            if min_heap and min_heap[0] < start:
18                heapq.heappop(min_heap)
19          
20            # Add the current interval's end time to the heap
21            heapq.heappush(min_heap, end)
22
23        # The length of the heap gives the minimum number of overlapping groups.
24        return len(min_heap)
25
26# Example usage:
27# sol = Solution()
28# ans = sol.minGroups([[1,3],[2,4],[3,5]])
29# print(ans)  # Expected output would be 2
30
1import java.util.Arrays;
2import java.util.PriorityQueue;
3
4class Solution {
5  
6    /**
7    * This method calculates the minimum number of groups needed such that
8    * each group consists of non-overlapping intervals from the given array of intervals.
9    * Intervals are considered to have a start and an end, and overlapping intervals cannot
10    * be in the same group.
11    *
12    * @param intervals Array of intervals where each interval is represented by a pair [start, end].
13    * @return The minimum number of groups required.
14    */
15    public int minGroups(int[][] intervals) {
16        // Sort the intervals based on the start time
17        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
18      
19        // A priority queue to manage the end times of the intervals
20        PriorityQueue<Integer> endTimeQueue = new PriorityQueue<>();
21      
22        // Iterate over all intervals
23        for (int[] interval : intervals) {
24            // If the queue is not empty and the smallest end time is less than the
25            // start of the current interval, we can reuse this group for the new interval
26            if (!endTimeQueue.isEmpty() && endTimeQueue.peek() < interval[0]) {
27                endTimeQueue.poll(); // Remove the interval with the smallest end time
28            }
29          
30            // Add the current interval's end time to the queue
31            endTimeQueue.offer(interval[1]);
32        }
33      
34        // The size of the priority queue indicates the minimum number of groups needed
35        return endTimeQueue.size();
36    }
37}
38
1#include <vector>
2#include <queue>
3#include <algorithm>
4
5class Solution {
6public:
7    // Function to find the minimum number of groups required
8    // so that no two intervals overlap within the same group.
9    int minGroups(vector<vector<int>>& intervals) {
10        // Sort the intervals based on their start times.
11        std::sort(intervals.begin(), intervals.end());
12
13        // Priority queue to store the end times of intervals.
14        // The queue is ordered such that the smallest end time is at the top.
15        std::priority_queue<int, std::vector<int>, std::greater<int>> endTimes;
16
17        // Iterate over the sorted intervals
18        for (const auto& interval : intervals) {
19            // If the priority queue is not empty and the smallest end time is 
20            // less than the current interval's start time:
21            if (!endTimes.empty() && endTimes.top() < interval[0]) {
22                // This means the current interval does not overlap with the interval
23                // that has the earliest end time. So, we can recycle this group.
24                endTimes.pop();
25            }
26            // Push the current interval's end time into the priority queue.
27            // This indicates that we have either used an existing group or created a new one.
28            endTimes.push(interval[1]);
29        }
30
31        // The size of the priority queue indicates the number of groups required.
32        // Each group has no overlapping intervals.
33        return endTimes.size();
34    }
35};
36
1// Define the type for an interval.
2type Interval = [number, number];
3
4// Function to compare two intervals based on their start time.
5function compareIntervals(a: Interval, b: Interval): number {
6    return a[0] - b[0];
7}
8
9// Function to find the minimum number of groups required
10// so that no two intervals overlap within the same group.
11function minGroups(intervals: Interval[]): number {
12    // Sort the intervals based on their start times.
13    intervals.sort(compareIntervals);
14
15    // Array to store the end times of intervals, functions like a priority queue.
16    // This array is kept sorted such that the smallest end time is at the start.
17    let endTimes: number[] = [];
18
19    // Iterate over the sorted intervals
20    for (const interval of intervals) {
21        // Check if there is an existing group that this interval can join
22        // without overlapping. This is the case if the earliest ending group
23        // ends before the current interval starts.
24        let canJoinGroup = false;
25        for (let i = 0; i < endTimes.length; ++i) {
26            if (endTimes[i] < interval[0]) {
27                // The current interval can join this group, so update the end time.
28                endTimes[i] = interval[1];
29                canJoinGroup = true;
30                break;
31            }
32        }
33
34        // If the current interval could not join any group, create a new one.
35        if (!canJoinGroup) {
36            endTimes.push(interval[1]);
37        }
38
39        // Keep the endTimes array sorted, so the smallest end time is at the start.
40        endTimes.sort((a, b) => a - b);
41    }
42
43    // The length of the endTimes array indicates the number of groups needed.
44    // Each group contains no overlapping intervals.
45    return endTimes.length;
46}
47
Not Sure What to Study? Take the 2-min Quiz๏ผš

Which two pointer techniques do you use to check if a string is a palindrome?

Time and Space Complexity

Time Complexity

The given code consists of three main parts: sorting the intervals, iterating through the sorted intervals, and manipulating a heap for each interval.

  1. The sort operation at the beginning has a time complexity of O(N log N), where N is the number of intervals, because TimSort (Python's built-in sorting algorithm) is used.

  2. Iterating through the sorted intervals has a linear time complexity of O(N) since we go through all intervals once.

  3. For each interval, we might perform a heap pop and heap push operation, both of which have a time complexity of O(log K), where K is the number of elements in the heap. In the worst-case scenario, each interval might be overlapping with all others, which would mean K can approach N, so we consider O(log N) as the complexity for these heap operations.

Combining these, the overall worst-case time complexity is O(N log N + N log N) which simplifies to O(N log N), the dominant term being the sort and heap operations.

Space Complexity

The additional space used by the code is for the heap h. In the worst case, the heap can contain all N intervals if none of them can be merged, which means the space complexity is O(N) for storing heap elements.

Therefore, the overall space complexity of the code is O(N).

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings


Got a question?ย Ask the Teaching Assistantย anything you don't understand.

Still not clear? Ask in the Forum, ย Discordย orย Submitย the part you don't understand to our editors.

โ†
โ†‘TA ๐Ÿ‘จโ€๐Ÿซ