Facebook Pixel

731. My Calendar II

Problem Description

You need to implement a calendar system that can handle event bookings while preventing triple bookings. A triple booking occurs when three events overlap at any point in time.

Each event is represented by a [startTime, endTime) interval, which means the event includes the startTime but excludes the endTime (half-open interval). For example, [10, 20) means the event runs from time 10 (inclusive) to time 20 (exclusive).

You need to implement a MyCalendarTwo class with two methods:

  1. MyCalendarTwo(): Initializes an empty calendar object
  2. book(startTime, endTime): Attempts to add a new event to the calendar
    • Returns true if the event can be added without causing a triple booking (no point in time has 3 or more overlapping events)
    • Returns false if adding this event would cause a triple booking, and the event should not be added

The solution uses a difference array approach with a SortedDict to track the number of concurrent events at each time point. When booking an event:

  • It increments the count at startTime by 1 (event starts)
  • It decrements the count at endTime by 1 (event ends)
  • It then iterates through all time points in order, maintaining a running sum of active events
  • If at any point the running sum exceeds 2 (meaning 3+ events overlap), it reverts the changes and returns false
  • Otherwise, the booking is successful and returns true

This approach efficiently handles the constraint that at most 2 events can overlap at any given time.

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

Intuition

The key insight is to think about how overlapping events affect the number of concurrent bookings at any point in time. Instead of storing complete intervals and checking overlaps between every pair of events, we can track the "change points" where events start and end.

Imagine a timeline where events are happening. When an event starts at time t, the number of concurrent events increases by 1. When an event ends at time t, the number of concurrent events decreases by 1. This is the core idea behind the difference array approach.

For example, if we have events [10, 20) and [15, 25):

  • At time 10: +1 event (total: 1)
  • At time 15: +1 event (total: 2)
  • At time 20: -1 event (total: 1)
  • At time 25: -1 event (total: 0)

By maintaining these change points in sorted order, we can efficiently calculate the number of concurrent events at any moment by summing up all the changes from the beginning up to that point. This running sum tells us exactly how many events are active.

The beauty of this approach is that we don't need to explicitly check every possible overlap combination. We just need to ensure that when we add a new event, the running sum never exceeds 2 at any point. If it would exceed 2, we know adding this event would create a triple booking.

Using a SortedDict allows us to maintain these change points in chronological order automatically, making it easy to iterate through them and compute the running sum. If we detect a triple booking, we can simply undo our changes by reverting the increments/decrements we just made.

Learn more about Segment Tree, Binary Search and Prefix Sum patterns.

Solution Approach

The solution uses a difference array pattern with a SortedDict data structure to efficiently track booking overlaps.

Data Structure Choice

We use a SortedDict (sorted dictionary) to store time points as keys and their corresponding changes as values. This keeps all time points automatically sorted, which is crucial for our algorithm.

Implementation Details

  1. Initialization: Create an empty SortedDict to store the difference array.

  2. Booking Process: When book(startTime, endTime) is called:

    • First, optimistically add the new event:
      • self.sd[startTime] = self.sd.get(startTime, 0) + 1 - increment the count at start time
      • self.sd[endTime] = self.sd.get(endTime, 0) - 1 - decrement the count at end time
  3. Validation: Check if this creates a triple booking:

    • Initialize a counter s = 0 to track concurrent events
    • Iterate through all values in the SortedDict in chronological order
    • For each time point, add its change value to the running sum: s += v
    • If at any point s > 2, we have detected a triple booking
  4. Handling Triple Booking:

    • If triple booking is detected, revert the changes:
      • self.sd[startTime] -= 1 - undo the increment at start time
      • self.sd[endTime] += 1 - undo the decrement at end time
    • Return False to indicate booking failed
  5. Successful Booking: If we complete the iteration without finding s > 2, return True

Time Complexity

  • Each book operation: O(n) where n is the number of unique time points stored
  • The SortedDict maintains order, so insertion is O(log n) but iteration is O(n)

Space Complexity

  • O(n) where n is the number of unique time points across all bookings

The elegance of this approach lies in its simplicity - by tracking only the changes at boundary points rather than full intervals, we can efficiently determine the maximum overlap with a single pass through the sorted time points.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a sequence of bookings to see how the difference array approach works:

Initial State: sd = {} (empty SortedDict)

Booking 1: book(10, 20)

  1. Add changes: sd[10] = 1, sd[20] = -1
  2. SortedDict now: {10: 1, 20: -1}
  3. Check for triple booking:
    • Start with s = 0
    • At time 10: s = 0 + 1 = 1 (1 concurrent event) ✓
    • At time 20: s = 1 + (-1) = 0 (0 concurrent events) ✓
  4. Maximum concurrent events = 1, no triple booking → Return true

Booking 2: book(15, 25)

  1. Add changes: sd[15] = 1, sd[25] = -1
  2. SortedDict now: {10: 1, 15: 1, 20: -1, 25: -1}
  3. Check for triple booking:
    • Start with s = 0
    • At time 10: s = 0 + 1 = 1 (1 concurrent event) ✓
    • At time 15: s = 1 + 1 = 2 (2 concurrent events) ✓
    • At time 20: s = 2 + (-1) = 1 (1 concurrent event) ✓
    • At time 25: s = 1 + (-1) = 0 (0 concurrent events) ✓
  4. Maximum concurrent events = 2, no triple booking → Return true

Booking 3: book(12, 18) (This will overlap with both existing events)

  1. Add changes: sd[12] = 1, sd[18] = -1
  2. SortedDict now: {10: 1, 12: 1, 15: 1, 18: -1, 20: -1, 25: -1}
  3. Check for triple booking:
    • Start with s = 0
    • At time 10: s = 0 + 1 = 1 (1 concurrent event) ✓
    • At time 12: s = 1 + 1 = 2 (2 concurrent events) ✓
    • At time 15: s = 2 + 1 = 3 (3 concurrent events) ✗ Triple booking detected!
  4. Revert changes:
    • sd[12] -= 1 → removes the entry (becomes 0)
    • sd[18] += 1 → removes the entry (becomes 0)
  5. SortedDict reverts to: {10: 1, 15: 1, 20: -1, 25: -1}
  6. Return false

The key insight: Between times 15 and 18, all three events would have overlapped (events [10,20), [15,25), and [12,18)), creating a triple booking. The algorithm detected this when the running sum reached 3 at time 15.

Solution Implementation

1from sortedcontainers import SortedDict
2
3class MyCalendarTwo:
4    def __init__(self):
5        # SortedDict maintains keys in sorted order
6        # Keys represent time points, values represent the change in number of bookings
7        # Positive value: booking starts, Negative value: booking ends
8        self.booking_events = SortedDict()
9
10    def book(self, startTime: int, endTime: int) -> bool:
11        """
12        Attempts to book a time slot from startTime to endTime.
13        Returns True if booking is successful (no triple booking), False otherwise.
14      
15        Args:
16            startTime: The start time of the booking (inclusive)
17            endTime: The end time of the booking (exclusive)
18      
19        Returns:
20            bool: True if booking successful, False if it would cause triple booking
21        """
22        # Add the new booking tentatively
23        # Increment counter at start time (booking begins)
24        self.booking_events[startTime] = self.booking_events.get(startTime, 0) + 1
25        # Decrement counter at end time (booking ends)
26        self.booking_events[endTime] = self.booking_events.get(endTime, 0) - 1
27      
28        # Check if this causes triple booking by scanning through all time points
29        current_bookings = 0
30        for booking_change in self.booking_events.values():
31            current_bookings += booking_change
32          
33            # If at any point we have more than 2 concurrent bookings
34            if current_bookings > 2:
35                # Revert the tentative booking
36                self.booking_events[startTime] -= 1
37                self.booking_events[endTime] += 1
38              
39                # Clean up: remove entries with 0 value to keep dictionary clean
40                if self.booking_events[startTime] == 0:
41                    del self.booking_events[startTime]
42                if self.booking_events[endTime] == 0:
43                    del self.booking_events[endTime]
44                  
45                return False
46      
47        # Booking successful - no triple booking detected
48        return True
49
50
51# Your MyCalendarTwo object will be instantiated and called as such:
52# obj = MyCalendarTwo()
53# param_1 = obj.book(startTime, endTime)
54
1class MyCalendarTwo {
2    // TreeMap to store event boundaries with their count changes
3    // Key: time point, Value: change in number of overlapping events at this point
4    private final Map<Integer, Integer> eventBoundaries = new TreeMap<>();
5
6    public MyCalendarTwo() {
7    }
8
9    public boolean book(int startTime, int endTime) {
10        // Increment count at start time (new event starts)
11        eventBoundaries.merge(startTime, 1, Integer::sum);
12        // Decrement count at end time (event ends)
13        eventBoundaries.merge(endTime, -1, Integer::sum);
14      
15        // Check if adding this event causes triple booking
16        int currentOverlaps = 0;
17        for (int countChange : eventBoundaries.values()) {
18            currentOverlaps += countChange;
19          
20            // If we have more than 2 overlapping events, reject this booking
21            if (currentOverlaps > 2) {
22                // Rollback the changes we just made
23                eventBoundaries.merge(startTime, -1, Integer::sum);
24                eventBoundaries.merge(endTime, 1, Integer::sum);
25                return false;
26            }
27        }
28      
29        // Booking is valid (no triple booking detected)
30        return true;
31    }
32}
33
34/**
35 * Your MyCalendarTwo object will be instantiated and called as such:
36 * MyCalendarTwo obj = new MyCalendarTwo();
37 * boolean param_1 = obj.book(startTime,endTime);
38 */
39
1class MyCalendarTwo {
2public:
3    MyCalendarTwo() {
4        // Constructor - initializes an empty calendar
5    }
6
7    bool book(int startTime, int endTime) {
8        // Tentatively add the new event by incrementing at start and decrementing at end
9        eventBoundaries[startTime]++;
10        eventBoundaries[endTime]--;
11      
12        // Check if this booking would cause a triple booking
13        int activeEvents = 0;
14        for (const auto& [time, delta] : eventBoundaries) {
15            activeEvents += delta;
16          
17            // If at any point we have more than 2 overlapping events, it's a triple booking
18            if (activeEvents > 2) {
19                // Rollback the tentative booking
20                eventBoundaries[startTime]--;
21                eventBoundaries[endTime]++;
22              
23                // Remove entries with value 0 to keep map clean
24                if (eventBoundaries[startTime] == 0) {
25                    eventBoundaries.erase(startTime);
26                }
27                if (eventBoundaries[endTime] == 0) {
28                    eventBoundaries.erase(endTime);
29                }
30              
31                return false;
32            }
33        }
34      
35        // Booking is valid - no triple bookings detected
36        return true;
37    }
38
39private:
40    // Map to store event boundaries
41    // Key: time point, Value: change in number of active events (+1 for start, -1 for end)
42    // This allows efficient tracking of overlapping intervals using a sweep line algorithm
43    map<int, int> eventBoundaries;
44};
45
46/**
47 * Your MyCalendarTwo object will be instantiated and called as such:
48 * MyCalendarTwo* obj = new MyCalendarTwo();
49 * bool param_1 = obj->book(startTime,endTime);
50 */
51
1// Map to track boundary events: positive for start times, negative for end times
2const boundaryEvents: Record<number, number> = {};
3
4/**
5 * Books a calendar event and checks if it causes triple booking
6 * @param startTime - The start time of the event
7 * @param endTime - The end time of the event
8 * @returns true if the event can be booked without causing triple booking, false otherwise
9 */
10function book(startTime: number, endTime: number): boolean {
11    // Increment counter at start time (event begins)
12    boundaryEvents[startTime] = (boundaryEvents[startTime] ?? 0) + 1;
13  
14    // Decrement counter at end time (event ends)
15    boundaryEvents[endTime] = (boundaryEvents[endTime] ?? 0) - 1;
16  
17    // Check if this booking causes triple booking by scanning through all time points
18    let activeEvents = 0;
19  
20    for (const eventCount of Object.values(boundaryEvents)) {
21        activeEvents += eventCount;
22      
23        // If more than 2 events overlap at any point, triple booking detected
24        if (activeEvents > 2) {
25            // Rollback the changes made to boundary events
26            if (--boundaryEvents[startTime] === 0) {
27                delete boundaryEvents[startTime];
28            }
29            if (++boundaryEvents[endTime] === 0) {
30                delete boundaryEvents[endTime];
31            }
32          
33            // Booking failed due to triple booking
34            return false;
35        }
36    }
37  
38    // Booking successful, no triple booking occurred
39    return true;
40}
41

Time and Space Complexity

The time complexity is O(n²) where n is the number of bookings.

For each book() operation:

  • Adding/updating entries in the SortedDict takes O(log n) time for each operation (two operations: one for startTime and one for endTime)
  • Iterating through all values in the SortedDict to check the overlap count takes O(n) time in the worst case, as the dictionary can have up to 2n entries (each booking adds at most 2 entries)
  • Since we perform this for each of the n bookings, the total time complexity becomes O(n) * O(n) = O(n²)

The space complexity is O(n) where n is the number of bookings.

The SortedDict stores at most 2n entries (in the worst case, each booking creates two new entries - one for start time and one for end time), which requires O(n) space.

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

Common Pitfalls

1. Forgetting to Clean Up Zero-Value Entries

The Pitfall: After reverting a failed booking, if you don't remove entries with value 0 from the SortedDict, the dictionary will accumulate unnecessary entries over time. This leads to:

  • Increased memory usage
  • Slower iteration in future book() calls
  • Potential confusion when debugging

Example of the Problem:

# Without cleanup - BAD
if current_bookings > 2:
    self.booking_events[startTime] -= 1
    self.booking_events[endTime] += 1
    # Dictionary might now have {10: 0, 20: 0, ...} entries
    return False

Solution:

# With cleanup - GOOD
if current_bookings > 2:
    self.booking_events[startTime] -= 1
    self.booking_events[endTime] += 1
  
    # Remove zero entries to keep dictionary clean
    if self.booking_events[startTime] == 0:
        del self.booking_events[startTime]
    if self.booking_events[endTime] == 0:
        del self.booking_events[endTime]
  
    return False

2. Using Regular Dict Instead of SortedDict

The Pitfall: Using a regular Python dict will cause incorrect results because the algorithm relies on processing time points in chronological order. Even though Python 3.7+ maintains insertion order, this doesn't help when keys are inserted out of chronological sequence.

Example of the Problem:

# Using regular dict - WRONG
self.booking_events = {}  # Regular dict

# If bookings come in order: book(20, 30), then book(10, 15)
# Dict iteration would be: {20: 1, 30: -1, 10: 1, 15: -1}
# This breaks the chronological scanning logic

Solution:

# Using SortedDict - CORRECT
from sortedcontainers import SortedDict
self.booking_events = SortedDict()

# Now iteration always happens in sorted key order: {10: 1, 15: -1, 20: 1, 30: -1}

3. Incorrect Reversion Logic

The Pitfall: When reverting a failed booking, using the wrong signs for the adjustments. Remember that you need to undo exactly what was done.

Example of the Problem:

# Initial add (correct):
self.booking_events[startTime] = self.booking_events.get(startTime, 0) + 1
self.booking_events[endTime] = self.booking_events.get(endTime, 0) - 1

# Incorrect reversion - WRONG
if current_bookings > 2:
    self.booking_events[startTime] += 1  # Should be -= 1
    self.booking_events[endTime] -= 1     # Should be += 1

Solution:

# Correct reversion
if current_bookings > 2:
    self.booking_events[startTime] -= 1  # Undo the +1
    self.booking_events[endTime] += 1    # Undo the -1

4. Not Using .get() Method for New Keys

The Pitfall: Directly incrementing a key that might not exist in the dictionary will raise a KeyError.

Example of the Problem:

# This will raise KeyError if startTime is not in the dict - WRONG
self.booking_events[startTime] += 1

Solution:

# Use .get() with default value - CORRECT
self.booking_events[startTime] = self.booking_events.get(startTime, 0) + 1

5. Early Return in the Validation Loop

The Pitfall: Returning True as soon as you find a point where current_bookings <= 2, instead of checking all time points.

Example of the Problem:

# WRONG - returns too early
for booking_change in self.booking_events.values():
    current_bookings += booking_change
    if current_bookings <= 2:
        return True  # This is wrong! Need to check ALL points
    if current_bookings > 2:
        # revert and return False

Solution:

# CORRECT - check all points before confirming success
for booking_change in self.booking_events.values():
    current_bookings += booking_change
    if current_bookings > 2:
        # revert and return False
      
# Only return True after checking ALL points
return True
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More