Facebook Pixel

2406. Divide Intervals Into Minimum Number of Groups

Problem Description

You are given a 2D integer array intervals where intervals[i] = [left_i, right_i] represents an inclusive interval [left_i, right_i].

Your task is to divide these intervals into groups with the following requirements:

  • Each interval must be placed in exactly one group
  • No two intervals in the same group can intersect with each other

You need to find the minimum number of groups required to accommodate all intervals.

Two intervals are considered to intersect if they share at least one common number. For example:

  • [1, 5] and [5, 8] intersect because they both contain the number 5
  • [1, 3] and [4, 6] do not intersect because they have no common numbers

The solution uses a greedy approach with a min heap. After sorting intervals by their left endpoints, it maintains a heap of the rightmost endpoints of each group. For each interval, if its left endpoint is greater than the minimum right endpoint in the heap (meaning it doesn't overlap with that group), it can reuse that group by replacing the old right endpoint with the current interval's right endpoint. Otherwise, a new group is needed. The final number of groups is the size of the heap.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Think of this problem like scheduling meetings in conference rooms. Each interval is a meeting that needs a room, and overlapping meetings cannot share the same room.

The key insight is that at any point in time, the number of groups we need equals the maximum number of intervals that overlap at that moment. This is similar to asking: "What's the maximum number of meetings happening simultaneously?"

To efficiently track this, we can process intervals in order of their start times. When we encounter a new interval, we need to decide: can it reuse an existing group (room), or do we need a new one?

Here's the critical observation: if we have multiple groups available, which one should we assign the current interval to? We should choose the group that ends earliest (has the smallest right endpoint). Why? Because this greedy choice maximizes our chances of reusing groups for future intervals.

This naturally leads us to use a min heap to track the right endpoints of all active groups. The heap always gives us quick access to the group that ends earliest:

  • If the current interval's left endpoint > heap's minimum (earliest ending group), the current interval can reuse that group since they don't overlap
  • Otherwise, all existing groups overlap with the current interval, so we need a new group

By maintaining this heap and processing intervals in sorted order, we ensure we're always making the optimal decision about group assignment. The final heap size represents the minimum number of groups needed, as it contains the right endpoint of each group that couldn't be merged with any other.

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

Solution Approach

The implementation follows a greedy strategy combined with a min heap data structure:

Step 1: Sort the intervals

sorted(intervals)

We sort all intervals by their left endpoints. This ensures we process intervals in chronological order of their start times, which is crucial for making optimal grouping decisions.

Step 2: Initialize a min heap

q = []

We use a min heap q to track the right endpoints of each group. The heap maintains the invariant that the smallest right endpoint (the group that ends earliest) is always at the top.

Step 3: Process each interval For each interval [left, right] in sorted order:

if q and q[0] < left:
    heappop(q)
  • Check if the heap is not empty and if the minimum right endpoint in the heap (top element q[0]) is less than the current interval's left endpoint
  • If true, it means the current interval doesn't overlap with the group that ends earliest
  • We can reuse this group by removing its old right endpoint with heappop(q)
heappush(q, right)
  • Push the current interval's right endpoint into the heap
  • This either represents updating an existing group (if we popped earlier) or creating a new group

Step 4: Return the result

return len(q)

The size of the heap at the end represents the minimum number of groups needed. Each element in the heap corresponds to one group's right endpoint.

Time Complexity: O(n log n) where n is the number of intervals - sorting takes O(n log n) and each heap operation takes O(log n) for n intervals.

Space Complexity: O(n) for the heap which can contain at most n elements in the worst case when all intervals overlap.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with intervals: [[0,3], [5,8], [1,5], [6,10], [2,7]]

Step 1: Sort intervals by left endpoint After sorting: [[0,3], [1,5], [2,7], [5,8], [6,10]]

Step 2: Process each interval with the min heap

IntervalHeap BeforeActionHeap AfterExplanation
[0,3][]Add new group[3]First interval, create Group 1
[1,5][3]Check: 3 < 1? No → Add new group[3,5]Overlaps with [0,3], need Group 2
[2,7][3,5]Check: 3 < 2? No → Add new group[3,5,7]Overlaps with existing groups, need Group 3
[5,8][3,5,7]Check: 3 < 5? Yes → Pop 3, Push 8[5,7,8]Can reuse Group 1 (ending at 3)
[6,10][5,7,8]Check: 5 < 6? Yes → Pop 5, Push 10[7,8,10]Can reuse Group 2 (ending at 5)

Visual Timeline:

Group 1: [0,3]--------[5,8]
Group 2:   [1,5]----------[6,10]
Group 3:     [2,7]
         0 1 2 3 4 5 6 7 8 9 10

Result: The heap has 3 elements, so we need 3 groups minimum.

The key insight: When we process [5,8], the heap's minimum is 3 (Group 1's endpoint). Since 3 < 5, interval [5,8] doesn't overlap with Group 1, so we can assign it there. Similarly, [6,10] can reuse Group 2. The heap always tells us which group ends earliest and is therefore the best candidate for reuse.

Solution Implementation

1import heapq
2from typing import List
3
4class Solution:
5    def minGroups(self, intervals: List[List[int]]) -> int:
6        # Min heap to track the end times of active groups
7        min_heap = []
8      
9        # Sort intervals by their start time
10        for start, end in sorted(intervals):
11            # If heap is not empty and the earliest ending group has finished
12            # before the current interval starts, we can reuse that group
13            if min_heap and min_heap[0] < start:
14                heapq.heappop(min_heap)
15          
16            # Add the current interval's end time to the heap
17            # This either creates a new group or reuses an existing one
18            heapq.heappush(min_heap, end)
19      
20        # The size of the heap represents the minimum number of groups needed
21        # Each element in the heap represents an active group
22        return len(min_heap)
23
1class Solution {
2    public int minGroups(int[][] intervals) {
3        // Sort intervals by start time in ascending order
4        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
5      
6        // Min heap to track the end times of groups
7        // Each element represents the latest end time of a group
8        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
9      
10        // Process each interval
11        for (int[] interval : intervals) {
12            // If the earliest ending group finishes before current interval starts,
13            // we can reuse that group
14            if (!minHeap.isEmpty() && minHeap.peek() < interval[0]) {
15                minHeap.poll();  // Remove the group that can be reused
16            }
17          
18            // Add current interval's end time to the heap
19            // This either creates a new group or updates the reused group's end time
20            minHeap.offer(interval[1]);
21        }
22      
23        // The size of the heap represents the minimum number of groups needed
24        return minHeap.size();
25    }
26}
27
1class Solution {
2public:
3    int minGroups(vector<vector<int>>& intervals) {
4        // Sort intervals by start time (ascending order)
5        sort(intervals.begin(), intervals.end());
6      
7        // Min heap to store the end times of groups
8        // Each element represents the latest end time of a group
9        priority_queue<int, vector<int>, greater<int>> minHeap;
10      
11        // Process each interval
12        for (auto& interval : intervals) {
13            int startTime = interval[0];
14            int endTime = interval[1];
15          
16            // If there exists a group whose end time is before current interval's start,
17            // we can reuse that group (remove old end time, will add new one)
18            if (!minHeap.empty() && minHeap.top() < startTime) {
19                minHeap.pop();
20            }
21          
22            // Add current interval's end time to the heap
23            // This either creates a new group or updates an existing group's end time
24            minHeap.push(endTime);
25        }
26      
27        // The size of heap represents the minimum number of groups needed
28        // Each element in heap represents one active group
29        return minHeap.size();
30    }
31};
32
1/**
2 * Finds the minimum number of groups needed to allocate all intervals
3 * such that no two intervals in the same group overlap.
4 * 
5 * @param intervals - Array of intervals where each interval is [start, end]
6 * @returns The minimum number of groups needed
7 */
8function minGroups(intervals: number[][]): number {
9    // Sort intervals by their start time in ascending order
10    intervals.sort((a, b) => a[0] - b[0]);
11  
12    // Min heap to track the end times of active groups
13    // The smallest end time is at the front, representing the group that finishes earliest
14    const endTimesHeap = new PriorityQueue({ 
15        compare: (a, b) => a - b 
16    });
17  
18    // Process each interval in chronological order
19    for (const [startTime, endTime] of intervals) {
20        // If there's a group that has already finished before this interval starts,
21        // we can reuse that group instead of creating a new one
22        if (!endTimesHeap.isEmpty() && endTimesHeap.front() < startTime) {
23            // Remove the finished group's end time from the heap
24            endTimesHeap.dequeue();
25        }
26      
27        // Add the current interval's end time to the heap
28        // This either represents a new group or updates an existing reused group
29        endTimesHeap.enqueue(endTime);
30    }
31  
32    // The size of the heap represents the minimum number of groups needed
33    // Each element in the heap corresponds to an active group
34    return endTimesHeap.size();
35}
36

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity is dominated by two operations:

  1. Sorting the intervals: sorted(intervals) takes O(n × log n) time where n is the number of intervals
  2. The for loop runs n times, and in each iteration:
    • heappop(q) operation (when condition is met): O(log k) where k is the size of heap
    • heappush(q, right) operation: O(log k) where k is the size of heap

Since the heap size is at most n, each heap operation is O(log n). The total time for all heap operations is O(n × log n).

Therefore, the overall time complexity is O(n × log n) + O(n × log n) = O(n × log n).

Space Complexity: O(n)

The space complexity comes from:

  1. The heap q which can contain at most n elements in the worst case (when all intervals overlap)
  2. The sorted function creates a new sorted list which takes O(n) space

Therefore, the overall space complexity is O(n).

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

Common Pitfalls

1. Incorrect Overlap Condition

A critical pitfall is misunderstanding when intervals overlap. The problem states that intervals intersect if they share at least one common number, meaning [1, 5] and [5, 8] DO overlap because they both contain 5.

Incorrect Implementation:

# Wrong: This treats touching intervals as non-overlapping
if min_heap and min_heap[0] < start:  # Should be <= 
    heapq.heappop(min_heap)

Correct Implementation:

# Intervals can only be in the same group if they don't share ANY number
# So we can reuse a group only when: end_of_group < start_of_new_interval
if min_heap and min_heap[0] < start:
    heapq.heappop(min_heap)

2. Using Maximum Heap Instead of Minimum Heap

Some might think we need to track the latest ending intervals, but we actually need the earliest ending ones to make optimal reuse decisions.

Why min heap is correct:

  • We want to find groups that have already ended to reuse them
  • The group with the smallest right endpoint is most likely to have ended before a new interval starts
  • This greedy choice minimizes the total number of groups

3. Forgetting to Sort the Intervals

Processing intervals in random order breaks the algorithm's logic.

Without sorting:

# Wrong: Processing in arbitrary order
for start, end in intervals:  # Missing sort!
    # Logic fails because we might process [10, 15] before [1, 5]

With proper sorting:

for start, end in sorted(intervals):
    # Now we process intervals in chronological order of start times

4. Off-by-One Error in Overlap Check

Remember that intervals are inclusive on both ends. The condition heap[0] < start is correct because:

  • If heap[0] = 5 (group ends at 5) and start = 6 (new interval starts at 6), they don't overlap
  • If heap[0] = 5 and start = 5, they DO overlap (both contain 5)

Test your understanding:

  • [1, 3] and [4, 6]: Don't overlap (3 < 4) ✓
  • [1, 3] and [3, 6]: Do overlap (share 3) ✓
  • [1, 5] and [6, 10]: Don't overlap (5 < 6) ✓
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More