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:
MyCalendarTwo()
: Initializes an empty calendar objectbook(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
- Returns
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.
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
-
Initialization: Create an empty
SortedDict
to store the difference array. -
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 timeself.sd[endTime] = self.sd.get(endTime, 0) - 1
- decrement the count at end time
- First, optimistically add the new event:
-
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
- Initialize a counter
-
Handling Triple Booking:
- If triple booking is detected, revert the changes:
self.sd[startTime] -= 1
- undo the increment at start timeself.sd[endTime] += 1
- undo the decrement at end time
- Return
False
to indicate booking failed
- If triple booking is detected, revert the changes:
-
Successful Booking: If we complete the iteration without finding
s > 2
, returnTrue
Time Complexity
- Each
book
operation:O(n)
wheren
is the number of unique time points stored - The
SortedDict
maintains order, so insertion isO(log n)
but iteration isO(n)
Space Complexity
O(n)
wheren
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 EvaluatorExample 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)
- Add changes:
sd[10] = 1
,sd[20] = -1
- SortedDict now:
{10: 1, 20: -1}
- 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) ✓
- Start with
- Maximum concurrent events = 1, no triple booking → Return
true
Booking 2: book(15, 25)
- Add changes:
sd[15] = 1
,sd[25] = -1
- SortedDict now:
{10: 1, 15: 1, 20: -1, 25: -1}
- 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) ✓
- Start with
- Maximum concurrent events = 2, no triple booking → Return
true
Booking 3: book(12, 18)
(This will overlap with both existing events)
- Add changes:
sd[12] = 1
,sd[18] = -1
- SortedDict now:
{10: 1, 12: 1, 15: 1, 18: -1, 20: -1, 25: -1}
- 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!
- Start with
- Revert changes:
sd[12] -= 1
→ removes the entry (becomes 0)sd[18] += 1
→ removes the entry (becomes 0)
- SortedDict reverts to:
{10: 1, 15: 1, 20: -1, 25: -1}
- 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 to2n
entries (each booking adds at most 2 entries) - Since we perform this for each of the
n
bookings, the total time complexity becomesO(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
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Want a Structured Path to Master System Design Too? Don’t Miss This!