My Calendar II
MedPrompt
You are implementing a program to use as your calendar. We can add a new event if adding the event will not cause a triple booking.
A triple booking happens when three events have some non-empty intersection (i.e., some moment is common to all the three events.).
The event can be represented as a pair of integers startTime and endTime that represents a booking on the half-open interval [startTime, endTime), the range of real numbers x such that startTime <= x < endTime.
Implement the MyCalendarTwo class:
MyCalendarTwo()Initializes the calendar object.boolean book(int startTime, int endTime)Returnstrueif the event can be added to the calendar successfully without causing a triple booking. Otherwise, returnfalseand do not add the event to the calendar.
Example 1:
Input
["MyCalendarTwo", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]]
Output
[null, true, true, true, false, true, true]
Explanation
MyCalendarTwo myCalendarTwo = new MyCalendarTwo();
myCalendarTwo.book(10, 20); // return True, The event can be booked.
myCalendarTwo.book(50, 60); // return True, The event can be booked.
myCalendarTwo.book(10, 40); // return True, The event can be double booked.
myCalendarTwo.book(5, 15); // return False, The event cannot be booked, because it would result in a triple booking.
myCalendarTwo.book(5, 10); // return True, The event can be booked, as it does not use time 10 which is already double booked.
myCalendarTwo.book(25, 55); // return True, The event can be booked, as the time in [25, 40) will be double booked with the third event, the time [40, 50) will be single booked, and the time [50, 55) will be double booked with the second event.
Constraints:
0 <= start < end <= 109- At most
1000calls will be made tobook.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach maintains two lists: one for all booked events (bookings) and another for all double-booked time intervals (overlaps). When a new event is to be booked, we first check if it intersects with any of the existing double-booked intervals. If it does, it would create a triple booking, so we reject the event. If it doesn't, we accept the event, add it to our bookings list, and then update the overlaps list by checking the new event against all previously booked events.
Algorithm
- Initialize two lists,
bookingsto store all events andoverlapsto store all double-booked intervals. - When
book(start, end)is called:- Iterate through each interval
oin theoverlapslist. If the new event[start, end)intersects witho, it would create a triple booking. Returnfalse. - If no triple booking is found, iterate through the
bookingslist. For each existing eventb, calculate the intersection with[start, end). If the intersection is non-empty, add it to theoverlapslist. - Add the new event
[start, end)to thebookingslist. - Return
true.
- Iterate through each interval
Walkthrough
We initialize two lists, bookings and overlaps. For each book(start, end) call:
-
Check for Triple Booking: Iterate through the
overlapslist. For each interval[o_start, o_end)inoverlaps, check if it intersects with the new event[start, end). An intersection occurs ifstart < o_endandend > o_start. If an intersection is found, it means a triple booking would occur. We returnfalseimmediately. -
Update Overlaps: If no triple booking is detected, we proceed to add the event. But first, we must identify any new double-booked intervals created by this new event. We iterate through the
bookingslist. For each existing event[b_start, b_end), we calculate its intersection with[start, end). The intersection is[max(start, b_start), min(end, b_end)). If this intersection is non-empty (i.e., the start is less than the end), we add this new double-booked interval to theoverlapslist. -
Add Event: After updating the
overlaps, we add the new event[start, end)to thebookingslist. -
Return
trueto indicate a successful booking.
class MyCalendarTwo { private List<int[]> bookings; private List<int[]> overlaps; public MyCalendarTwo() { bookings = new ArrayList<>(); overlaps = new ArrayList<>(); } public boolean book(int start, int end) { for (int[] o : overlaps) { if (start < o[1] && end > o[0]) { return false; // Triple booking } } for (int[] b : bookings) { int overlapStart = Math.max(start, b[0]); int overlapEnd = Math.min(end, b[1]); if (overlapStart < overlapEnd) { overlaps.add(new int[]{overlapStart, overlapEnd}); } } bookings.add(new int[]{start, end}); return true; }}Complexity
Time
O(N^3) for N calls. The k-th call takes O(k^2) time because the number of overlaps can be O(k^2). The total time is the sum of k^2 for k from 1 to N.
Space
O(N^2), where N is the number of bookings. The `bookings` list stores N events, but the `overlaps` list can store up to O(N^2) intervals.
Trade-offs
Pros
Conceptually straightforward and relatively easy to implement.
Cons
Highly inefficient
O(N^3)time complexity forNcalls.High space complexity of
O(N^2).
This approach treats the start and end points of events as significant moments in time. We use a sorted map (like a TreeMap in Java) to store the change in the number of active events at each boundary point. A start point s increments the count (+1), and an end point e decrements it (-1). To check if a new event [start, end) can be booked, we tentatively add its boundaries to the map. Then, we sweep across the timeline by iterating through the map's keys, calculating the cumulative number of active events at each point. If this number ever reaches 3, a triple booking would occur.
Algorithm
- Use a
TreeMapto store boundary points and the change in active events (+1 for start, -1 for end). - When
book(start, end)is called:- Tentatively update the map:
map[start]++andmap[end]--. - Initialize an
active_eventscounter to 0. - Iterate through the map's values in chronological order. Sum up the changes to
active_events. - If
active_eventsever reaches 3, a triple booking is detected. Revert the changes to the map and returnfalse. - If the loop completes, the booking is valid. Return
true.
- Tentatively update the map:
Walkthrough
We use a TreeMap named delta to store boundary points as keys and the change in active event count as values.
For each book(start, end) call:
- Tentative Update: We tentatively add the new event's boundaries to the
deltamap. We increment the count atstartby 1 and decrement the count atendby 1.delta.put(start, delta.getOrDefault(start, 0) + 1);delta.put(end, delta.getOrDefault(end, 0) - 1); - Sweep and Check: We initialize an
active_eventscounter to 0. We iterate through the values of thedeltamap in chronological order (whichTreeMapprovides). At each time point, we add the corresponding delta value toactive_events. After each update, we check ifactive_eventsis 3 or more. - Handle Triple Booking: If
active_eventsreaches 3, it signifies a triple booking. We must revert the changes made in step 1 by decrementing the count atstartand incrementing it atend. Then, we returnfalse. - Confirm Booking: If the sweep completes without the active event count ever reaching 3, the booking is valid. The tentative changes are now permanent, and we return
true.
class MyCalendarTwo { private TreeMap<Integer, Integer> delta; public MyCalendarTwo() { delta = new TreeMap<>(); } public boolean book(int start, int end) { delta.put(start, delta.getOrDefault(start, 0) + 1); delta.put(end, delta.getOrDefault(end, 0) - 1); int activeEvents = 0; for (int d : delta.values()) { activeEvents += d; if (activeEvents >= 3) { // Revert changes delta.put(start, delta.get(start) - 1); if (delta.get(start) == 0) { delta.remove(start); } delta.put(end, delta.get(end) + 1); if (delta.get(end) == 0) { delta.remove(end); } return false; } } return true; }}Complexity
Time
O(N^2) for N calls. The k-th call involves adding two entries to the map (O(log k)) and then iterating through O(k) entries. So, each call is O(k). The total time is the sum of k from 1 to N.
Space
O(N), where N is the number of bookings. The `TreeMap` will store at most 2N boundary points.
Trade-offs
Pros
A significant improvement in time and space complexity over the brute-force approach.
Elegant solution based on the sweep-line paradigm.
Cons
The
O(N^2)time complexity might be too slow for a very large number of calls, although it passes the given constraints.
This is the most efficient approach, utilizing a segment tree data structure to handle the range updates and queries. The problem's time coordinates can be very large (10^9), so we use a dynamic segment tree (where nodes are created on-demand) to avoid allocating a massive array. Each node in the tree represents a time interval and stores the maximum number of overlapping events within that interval. Lazy propagation is used to efficiently apply updates to large ranges.
Algorithm
- Implement a dynamic Segment Tree with lazy propagation over the coordinate range
[0, 10^9]. - Each node in the tree stores
max_val(maximum bookings in its range) and alazytag for updates. - When
book(start, end)is called:- Query the segment tree for the range
[start, end - 1]to find the maximum number of existing overlaps. - If the result is 2 or more, return
falseas it would cause a triple booking. - Otherwise, update the segment tree for the range
[start, end - 1]by incrementing the booking count by 1. - Return
true.
- Query the segment tree for the range
Walkthrough
We build a dynamic segment tree over the range [0, 10^9]. Each node will store max_val, the maximum number of concurrent bookings in its corresponding interval, and lazy, a value for lazy propagation.
The book(start, end) logic is as follows:
- Query: Before making any changes, we query the segment tree for the interval
[start, end). This query will find the maximum number of events that are already booked at any single point within this new interval. - Check for Triple Booking: If the result of the query is 2 or more, it means some point in
[start, end)is already double-booked. Adding the new event would create a triple booking. In this case, we returnfalse. - Update: If the query result is less than 2, the booking is permissible. We then perform an update operation on the segment tree for the interval
[start, end), incrementing the booking count for this range by 1. This is done efficiently using lazy propagation. - Return
true.
The query(range) operation finds the maximum value in the given range, and the update(range, value) operation adds value to all elements in the given range. Both operations take O(log C) time, where C is the maximum coordinate value.
class MyCalendarTwo { private static class Node { int maxVal; int lazy; Node left, right; } private Node root; private final int MAX_COORD = 1_000_000_000; public MyCalendarTwo() { root = new Node(); } private void push(Node node) { if (node.lazy > 0) { if (node.left == null) node.left = new Node(); if (node.right == null) node.right = new Node(); node.left.lazy += node.lazy; node.left.maxVal += node.lazy; node.right.lazy += node.lazy; node.right.maxVal += node.lazy; node.lazy = 0; } } private void update(Node node, int start, int end, int l, int r, int val) { if (l > r || l > end || r < start) { return; } if (l >= start && r <= end) { node.lazy += val; node.maxVal += val; return; } push(node); int mid = l + (r - l) / 2; if (node.left == null) node.left = new Node(); if (node.right == null) node.right = new Node(); update(node.left, start, end, l, mid, val); update(node.right, start, end, mid + 1, r, val); node.maxVal = Math.max(node.left.maxVal, node.right.maxVal); } private int query(Node node, int start, int end, int l, int r) { if (l > r || node == null || l > end || r < start) { return 0; } if (l >= start && r <= end) { return node.maxVal; } push(node); int mid = l + (r - l) / 2; int leftQuery = query(node.left, start, end, l, mid); int rightQuery = query(node.right, start, end, mid + 1, r); return Math.max(leftQuery, rightQuery); } public boolean book(int start, int end) { int maxBookings = query(root, start, end - 1, 0, MAX_COORD); if (maxBookings >= 2) { return false; } update(root, start, end - 1, 0, MAX_COORD, 1); return true; }}Complexity
Time
O(N * log C), where N is the number of bookings and C is the maximum coordinate value (10^9). Each call to `book` performs a query and an update, both taking O(log C) time.
Space
O(N * log C), where N is the number of bookings and C is the maximum coordinate value. Each update on the dynamic segment tree can create O(log C) new nodes.
Trade-offs
Pros
Optimal time complexity, making it very fast for a large number of calls.
Handles a very large coordinate range efficiently.
Cons
Significantly more complex to implement correctly compared to other approaches.
The constant factors associated with the complexity might be larger, making it slower for very small N.
Solutions
Solution
class MyCalendarTwo { private Map < Integer , Integer > tm = new TreeMap <>(); public MyCalendarTwo () { } public boolean book ( int start , int end ) { tm . put ( start , tm . getOrDefault ( start , 0 ) + 1 ); tm . put ( end , tm . getOrDefault ( end , 0 ) - 1 ); int s = 0 ; for ( int v : tm . values ()) { s += v ; if ( s > 2 ) { tm . put ( start , tm . get ( start ) - 1 ); tm . put ( end , tm . get ( end ) + 1 ); return false ; } } return true ; } } /** * Your MyCalendarTwo object will be instantiated and called as such: * MyCalendarTwo obj = new MyCalendarTwo(); * boolean param_1 = obj.book(start,end); */var MyCalendarTwo = function () { this . events = []; this . overlaps = []; }; /** * @param {number} start * @param {number} end * @return {boolean} */ MyCalendarTwo . prototype . book = function ( start , end ) { for ( let [ s , e ] of this . overlaps ) { if ( Math . max ( start , s ) < Math . min ( end , e )) { return false ; } } for ( let [ s , e ] of this . events ) { if ( Math . max ( start , s ) < Math . min ( end , e )) { this . overlaps . push ([ Math . max ( start , s ), Math . min ( end , e )]); } } this . events . push ([ start , end ]); return true ; }; /** * Your MyCalendarTwo object will be instantiated and called as such: * var obj = new MyCalendarTwo() * var param_1 = obj.book(start,end) */class MyCalendarTwo { public: map < int , int > m ; MyCalendarTwo () { } bool book ( int start , int end ) { ++ m [ start ]; -- m [ end ]; int s = 0 ; for ( auto & [ _ , v ] : m ) { s += v ; if ( s > 2 ) { -- m [ start ]; ++ m [ end ]; return false ; } } return true ; } }; /** * Your MyCalendarTwo object will be instantiated and called as such: * MyCalendarTwo* obj = new MyCalendarTwo(); * bool param_1 = obj->book(start,end); */from sortedcontainers import SortedDict class MyCalendarTwo : def __init__ ( self ): self . sd = SortedDict () # cannot be dict like self.sd={} def book ( self , start : int , end : int ) -> bool : self . sd [ start ] = self . sd . get ( start , 0 ) + 1 self . sd [ end ] = self . sd . get ( end , 0 ) - 1 s = 0 for v in self . sd . values (): s += v if s > 2 : self . sd [ start ] -= 1 self . sd [ end ] += 1 return False return True # Your MyCalendarTwo object will be instantiated and called as such: # obj = MyCalendarTwo() # param_1 = obj.book(start,end) ############ class MyCalendarTwo ( object ): def __init__ ( self ): # 每个被book了的区间 self . booked = list () # 每个重叠了的区间 self . overlaped = list () def book ( self , start , end ): """ :type start: int :type end: int :rtype: bool """ for os , oe in self . overlaped : if max ( os , start ) < min ( oe , end ): return False for bs , be in self . booked : ss = max ( bs , start ) ee = min ( be , end ) if ss < ee : self . overlaped . append (( ss , ee )) self . booked . append (( start , end )) return True # Your MyCalendarTwo object will be instantiated and called as such: # obj = MyCalendarTwo() # param_1 = obj.book(start,end)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.