3885. Design Event Manager
Problem Description
You are given an initial list of events, where each event has a unique eventId and a priority. Your task is to design a data structure that can manage these events efficiently, supporting priority updates and retrieval of the highest-priority event.
You need to implement the EventManager class with the following operations:
-
EventManager(int[][] events): Initializes the manager with the given events, whereevents[i] = [eventId_i, priority_i]. EacheventIdis unique. -
void updatePriority(int eventId, int newPriority): Updates the priority of the active event with ideventIdtonewPriority. (You can assume the giveneventIdcorresponds to an active event.) -
int pollHighest(): Removes and returns theeventIdof the active event with the highest priority. The rules for this operation are:- If multiple active events share the same highest priority, return the smallest
eventIdamong them. - If there are no active events remaining, return
-1.
- If multiple active events share the same highest priority, return the smallest
An event is considered active if it has not yet been removed by a pollHighest() call. In other words, once an event is polled, it is permanently removed and no longer participates in future operations.
Example walkthrough:
Suppose the events are [[1, 5], [2, 8], [3, 5]]:
pollHighest()returns2, because event2has the highest priority8. Event2is now removed.updatePriority(3, 10)changes the priority of event3from5to10.pollHighest()returns3, since event3now has priority10, the highest among the remaining active events.pollHighest()returns1, the only remaining active event.pollHighest()returns-1, since no active events are left.
Note the tie-breaking rule: when two active events have the same priority, the one with the smaller eventId should be returned first.
How We Pick the Algorithm
Why Design + Supporting Data Structures?
This problem maps to Design + Supporting Data Structures through a short path in the full flowchart.
The problem requires designing an EventManager class with sorted set and hash map to support priority updates and polling the highest-priority event.
Open in FlowchartIntuition
The core challenge of this problem is to repeatedly find the event with the highest priority (with ties broken by the smallest eventId), while also supporting dynamic priority updates and removals. This combination of "always get the best element" plus "modify elements on the fly" is the key signal that guides us toward the right data structure.
A first thought might be to use a heap (priority queue), since heaps are great at quickly retrieving the maximum element. However, a plain heap struggles with the updatePriority operation: when an event's priority changes, the old entry is buried somewhere inside the heap and cannot be efficiently located or removed. We would need extra bookkeeping (like lazy deletion) to handle stale entries, which complicates the logic.
This leads us to a cleaner idea: use a sorted set (an ordered collection that keeps its elements sorted at all times). With a sorted set, we can:
- Always access the "best" element directly from one end.
- Insert and remove any specific element efficiently while keeping everything sorted.
The next question is how to order the elements so that the "best" event sits at the front. We want the highest priority first, and among equal priorities, the smallest eventId first. A neat trick handles both conditions at once: store each event as the tuple (-priority, eventId). By negating the priority, the highest priority becomes the smallest negated value, so it naturally sorts to the front. And since tuples compare element by element, ties in -priority are automatically broken by the smaller eventId — exactly matching the tie-breaking rule.
There is still one missing piece. The updatePriority operation only gives us an eventId, but to remove the old tuple (-priority, eventId) from the sorted set, we need to know the event's current priority. To bridge this gap, we keep a hash map d that maps each eventId to its current priority. This lets us instantly look up the old priority, reconstruct the exact tuple to remove, and then insert the updated tuple.
Putting these two structures together gives us an elegant solution: the sorted set handles ordering and fast retrieval of the highest-priority event, while the hash map lets us locate any event's current priority so that updates and removals stay clean and efficient.
Pattern Learn more about Heap (Priority Queue) patterns.
Solution Approach
Solution 1: Sorted Set
We define a sorted set sl to store tuples of priority and id (-priority, eventId) for all active events, and a hash map d to store the priority of each event.
Initialization (__init__)
During initialization, we iterate over the given event list. For each event (eventId, priority):
- We add the tuple
(-priority, eventId)into the sorted setsl. The negation ofpriorityensures the highest priority sits at the front, and theeventIdas the second element automatically breaks ties in favor of the smaller id. - We record the event's priority in the hash map
d, i.e.,d[eventId] = priority, so we can look it up later.
Updating priority (updatePriority)
For the updatePriority(eventId, newPriority) operation:
- We first retrieve the old priority of the event from the hash map:
old_priority = d[eventId]. - We remove the old tuple
(-old_priority, eventId)from the sorted setsl. This is why we needed the hash map — without it, we wouldn't know the old priority required to reconstruct the exact tuple to delete. - We add the new tuple
(-newPriority, eventId)intosl. - Finally, we update the event's priority in the hash map:
d[eventId] = newPriority.
Polling the highest (pollHighest)
For the pollHighest() operation:
- We first check whether the sorted set
slis empty. If it is, we return-1because there are no active events. - Otherwise, the first element
sl.pop(0)is the event with the highest priority (and smallest id among ties, thanks to our tuple ordering). We extract itseventIdviasl.pop(0)[1]. - We delete the event's priority information from the hash map with
d.pop(eventId), keepingdconsistent with the active events insl. - We return the
eventId.
Data structures used
- Sorted set (
SortedList): maintains all active events in sorted order, allowing fast access to the highest-priority event from the front, as well as efficient insertion and removal of any specific element. - Hash map (
d): maps eacheventIdto its current priority, enablingupdatePriorityto locate and remove the old tuple inO(1)lookup time.
Complexity analysis
Let n be the number of active events.
- Time complexity: Each
updatePriorityandpollHighestoperation performs a constant number of sorted-set insertions, removals, or lookups, each costingO(log n). The hash map operations areO(1). Initialization costsO(n log n). - Space complexity:
O(n), for storing the active events in both the sorted set and the hash map.
Example Walkthrough
Let's trace through a small example to illustrate how the sorted set and hash map work together. Suppose we initialize the manager with the events [[1, 5], [2, 8], [3, 5]].
Step 1: Initialization (EventManager([[1, 5], [2, 8], [3, 5]]))
We process each event, inserting the tuple (-priority, eventId) into the sorted set sl and recording the priority in the hash map d.
- Event
(1, 5)→ add(-5, 1)tosl, setd[1] = 5 - Event
(2, 8)→ add(-8, 2)tosl, setd[2] = 8 - Event
(3, 5)→ add(-5, 3)tosl, setd[3] = 5
After initialization, the structures look like this:
sl = [(-8, 2), (-5, 1), (-5, 3)] # sorted ascending d = {1: 5, 2: 8, 3: 5}
Notice that (-8, 2) sits at the front because 8 is the highest priority. The tuples (-5, 1) and (-5, 3) share the same negated priority, so they're ordered by eventId, putting 1 before 3 — exactly the tie-breaking rule we want.
Step 2: pollHighest() → returns 2
We pop the first element of sl, which is (-8, 2). Its eventId is 2. We also remove 2 from the hash map.
sl = [(-5, 1), (-5, 3)] d = {1: 5, 3: 5}
Event 2 is now permanently removed. Returns 2.
Step 3: updatePriority(3, 10)
Event 3 changes its priority from 5 to 10.
- Look up the old priority:
d[3] = 5. - Remove the old tuple
(-5, 3)fromsl— the hash map told us exactly which tuple to delete. - Add the new tuple
(-10, 3)tosl. - Update the hash map:
d[3] = 10.
sl = [(-10, 3), (-5, 1)] # (-10, 3) moves to the front d = {1: 5, 3: 10}
The new tuple (-10, 3) automatically sorts to the front since -10 < -5.
Step 4: pollHighest() → returns 3
We pop (-10, 3) from the front. Event 3 now has the highest priority 10.
sl = [(-5, 1)] d = {1: 5}
Returns 3.
Step 5: pollHighest() → returns 1
We pop (-5, 1), the only remaining event.
sl = [] d = {}
Returns 1.
Step 6: pollHighest() → returns -1
The sorted set sl is now empty, so there are no active events left. Returns -1.
Summary of the trace
| Operation | Action | sl after | Result |
|---|---|---|---|
init | insert all events | [(-8,2), (-5,1), (-5,3)] | — |
pollHighest() | pop (-8, 2) | [(-5,1), (-5,3)] | 2 |
updatePriority(3,10) | remove (-5,3), add (-10,3) | [(-10,3), (-5,1)] | — |
pollHighest() | pop (-10, 3) | [(-5,1)] | 3 |
pollHighest() | pop (-5, 1) | [] | 1 |
pollHighest() | empty set | [] | -1 |
This trace shows how negating the priority lets the highest-priority event sit at the front, how the tuple's second element cleanly handles ties, and how the hash map allows us to locate and remove the exact stale tuple during a priority update.
Solution Implementation
1from sortedcontainers import SortedList
2
3
4class EventManager:
5
6 def __init__(self, events: list[list[int]]):
7 # SortedList stores tuples of (-priority, event_id) so that the
8 # highest priority (and smallest event_id on ties) comes first.
9 self.sorted_list = SortedList()
10 # Maps event_id -> current priority for O(1) priority lookups.
11 self.priority_map = {}
12 for event_id, priority in events:
13 self.sorted_list.add((-priority, event_id))
14 self.priority_map[event_id] = priority
15
16 def updatePriority(self, eventId: int, newPriority: int) -> None:
17 # Fetch the existing priority to locate the old entry.
18 old_priority = self.priority_map[eventId]
19 # Remove the stale (negative priority, id) tuple from the sorted list.
20 self.sorted_list.remove((-old_priority, eventId))
21 # Insert the updated entry with the new priority.
22 self.sorted_list.add((-newPriority, eventId))
23 # Update the lookup map to reflect the new priority.
24 self.priority_map[eventId] = newPriority
25
26 def pollHighest(self) -> int:
27 # No events left, return -1 as a sentinel value.
28 if not self.sorted_list:
29 return -1
30 # Pop the first element (highest priority) and extract its event_id.
31 event_id = self.sorted_list.pop(0)[1]
32 # Keep the map consistent by removing the polled event.
33 self.priority_map.pop(event_id)
34 return event_id
35
36
37# Your EventManager object will be instantiated and called as such:
38# obj = EventManager(events)
39# obj.updatePriority(eventId,newPriority)
40# param_2 = obj.pollHighest()
411class EventManager {
2 // TreeSet acts as an ordered structure to fetch the highest-priority event quickly.
3 // Each element is stored as {negativePriority, eventId} so that:
4 // - higher priority (stored as smaller negative value) comes first
5 // - ties are broken by smaller eventId
6 private TreeSet<int[]> sortedEvents;
7
8 // Maps an eventId to its current priority, enabling O(1) lookups during updates.
9 private Map<Integer, Integer> eventPriority;
10
11 /**
12 * Initializes the EventManager with a list of events.
13 *
14 * @param events array where each entry is {eventId, priority}
15 */
16 public EventManager(int[][] events) {
17 sortedEvents = new TreeSet<>((a, b) -> {
18 // Primary sort by stored priority (negated, so higher priority first).
19 if (a[0] != b[0]) {
20 return a[0] - b[0];
21 }
22 // Secondary sort by eventId to break ties deterministically.
23 return a[1] - b[1];
24 });
25
26 eventPriority = new HashMap<>();
27
28 for (int[] event : events) {
29 int eventId = event[0];
30 int priority = event[1];
31 // Store negated priority so the TreeSet orders highest priority first.
32 sortedEvents.add(new int[] {-priority, eventId});
33 eventPriority.put(eventId, priority);
34 }
35 }
36
37 /**
38 * Updates the priority of an existing event.
39 *
40 * @param eventId the id of the event to update
41 * @param newPriority the new priority value to assign
42 */
43 public void updatePriority(int eventId, int newPriority) {
44 // Retrieve the current priority to locate the existing entry.
45 int oldPriority = eventPriority.get(eventId);
46
47 // Remove the outdated entry from the ordered set.
48 sortedEvents.remove(new int[] {-oldPriority, eventId});
49
50 // Insert the updated entry with the new (negated) priority.
51 sortedEvents.add(new int[] {-newPriority, eventId});
52
53 // Refresh the priority record in the map.
54 eventPriority.put(eventId, newPriority);
55 }
56
57 /**
58 * Removes and returns the id of the highest-priority event.
59 *
60 * @return the eventId of the highest-priority event, or -1 if none remain
61 */
62 public int pollHighest() {
63 if (sortedEvents.isEmpty()) {
64 return -1;
65 }
66
67 // pollFirst() retrieves and removes the smallest element,
68 // which corresponds to the highest priority due to negation.
69 int[] top = sortedEvents.pollFirst();
70 int eventId = top[1];
71
72 // Keep the map in sync by removing the polled event.
73 eventPriority.remove(eventId);
74
75 return eventId;
76 }
77}
78
79/**
80 * Your EventManager object will be instantiated and called as such:
81 * EventManager obj = new EventManager(events);
82 * obj.updatePriority(eventId, newPriority);
83 * int param_2 = obj.pollHighest();
84 */
851class EventManager {
2public:
3 // Ordered set storing events as {-priority, eventId}.
4 // Negating the priority lets the std::set keep the highest-priority
5 // event at begin(), since the set sorts in ascending order.
6 set<pair<int, int>> eventSet;
7
8 // Maps an eventId to its current priority, enabling O(1) lookup
9 // when we need to update or erase a specific event.
10 unordered_map<int, int> priorityOf;
11
12 // Initialize the manager with the given list of events.
13 // Each event is represented as {eventId, priority}.
14 EventManager(vector<vector<int>>& events) {
15 for (auto& event : events) {
16 int eventId = event[0];
17 int priority = event[1];
18 // Insert with negated priority for max-at-front ordering.
19 eventSet.insert({-priority, eventId});
20 // Record the event's priority for future updates.
21 priorityOf[eventId] = priority;
22 }
23 }
24
25 // Change the priority of an existing event.
26 void updatePriority(int eventId, int newPriority) {
27 int oldPriority = priorityOf[eventId];
28 // Remove the entry that holds the stale priority.
29 eventSet.erase({-oldPriority, eventId});
30 // Re-insert the event with the updated priority.
31 eventSet.insert({-newPriority, eventId});
32 // Keep the lookup map in sync.
33 priorityOf[eventId] = newPriority;
34 }
35
36 // Remove and return the eventId with the highest priority.
37 // Returns -1 if no events remain.
38 int pollHighest() {
39 if (eventSet.empty()) {
40 return -1;
41 }
42 // The highest-priority event sits at the front of the set.
43 auto it = eventSet.begin();
44 int eventId = it->second;
45 // Erase it from both containers to fully remove the event.
46 eventSet.erase(it);
47 priorityOf.erase(eventId);
48 return eventId;
49 }
50};
51
52/**
53 * Your EventManager object will be instantiated and called as such:
54 * EventManager* obj = new EventManager(events);
55 * obj->updatePriority(eventId,newPriority);
56 * int param_2 = obj->pollHighest();
57 */
581// Ordered list storing events as [-priority, eventId].
2// Negating the priority lets us keep the highest-priority event at the
3// front (index 0), since the list is sorted in ascending order.
4let eventList: [number, number][] = [];
5
6// Maps an eventId to its current priority, enabling quick lookup
7// when we need to update or erase a specific event.
8let priorityOf: Map<number, number> = new Map();
9
10// Compare two [negPriority, eventId] pairs the same way std::set does:
11// first by the negated priority, then by the eventId.
12function comparePair(a: [number, number], b: [number, number]): number {
13 if (a[0] !== b[0]) {
14 return a[0] - b[0];
15 }
16 return a[1] - b[1];
17}
18
19// Binary search for the index where `target` should reside.
20// Returns the first index whose element is >= target (lower bound).
21function lowerBound(target: [number, number]): number {
22 let low = 0;
23 let high = eventList.length;
24 while (low < high) {
25 const mid = (low + high) >> 1;
26 if (comparePair(eventList[mid], target) < 0) {
27 low = mid + 1;
28 } else {
29 high = mid;
30 }
31 }
32 return low;
33}
34
35// Insert a pair into the sorted list at its correct position.
36function insertPair(pair: [number, number]): void {
37 const index = lowerBound(pair);
38 eventList.splice(index, 0, pair);
39}
40
41// Erase a pair from the sorted list if it exists.
42function erasePair(pair: [number, number]): void {
43 const index = lowerBound(pair);
44 // Confirm the element at `index` is exactly the target before removing.
45 if (index < eventList.length && comparePair(eventList[index], pair) === 0) {
46 eventList.splice(index, 1);
47 }
48}
49
50// Initialize the manager with the given list of events.
51// Each event is represented as [eventId, priority].
52function EventManager(events: number[][]): void {
53 // Reset global state in case of repeated instantiation.
54 eventList = [];
55 priorityOf = new Map();
56 for (const event of events) {
57 const eventId = event[0];
58 const priority = event[1];
59 // Insert with negated priority for max-at-front ordering.
60 insertPair([-priority, eventId]);
61 // Record the event's priority for future updates.
62 priorityOf.set(eventId, priority);
63 }
64}
65
66// Change the priority of an existing event.
67function updatePriority(eventId: number, newPriority: number): void {
68 const oldPriority = priorityOf.get(eventId)!;
69 // Remove the entry that holds the stale priority.
70 erasePair([-oldPriority, eventId]);
71 // Re-insert the event with the updated priority.
72 insertPair([-newPriority, eventId]);
73 // Keep the lookup map in sync.
74 priorityOf.set(eventId, newPriority);
75}
76
77// Remove and return the eventId with the highest priority.
78// Returns -1 if no events remain.
79function pollHighest(): number {
80 if (eventList.length === 0) {
81 return -1;
82 }
83 // The highest-priority event sits at the front of the list.
84 const front = eventList[0];
85 const eventId = front[1];
86 // Erase it from both containers to fully remove the event.
87 eventList.splice(0, 1);
88 priorityOf.delete(eventId);
89 return eventId;
90}
91Time and Space Complexity
Time Complexity:
-
Initialization (
__init__): The constructor iterates over allninitial events. For each event, it performsself.sl.add(...), which takesO(log n)time on aSortedList, and a dictionary insertion, which takesO(1)time. Therefore, the total initialization time isO(n log n). -
updatePriority: This method performs oneself.sl.remove(...)operation and oneself.sl.add(...)operation, each takingO(log n)time on aSortedList. The dictionary lookups and updates takeO(1)time. Thus, each call toupdatePrioritytakesO(log n)time. -
pollHighest: This method performs oneself.sl.pop(0)operation, which takesO(log n)time on aSortedList, along withO(1)dictionary operations. Therefore, each call topollHighesttakesO(log n)time.
Space Complexity:
The space complexity is O(n), where n is the number of active events. The SortedList self.sl stores up to n tuples, and the dictionary self.d stores up to n key-value pairs. Both structures grow linearly with the number of active events, so the overall space complexity is O(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting to negate the priority, leading to incorrect ordering
A very common mistake is storing tuples as (priority, eventId) instead of (-priority, eventId) in the SortedList. A SortedList keeps elements in ascending order, so its front (index 0) is the smallest element. If you store the raw priority, then pop(0) returns the lowest-priority event, which is the opposite of what pollHighest() requires.
Why the negation works for tie-breaking too: By storing (-priority, eventId), two events with the same priority will have the same first element -priority, so the SortedList falls back to comparing the second element eventId in ascending order. This automatically returns the smallest eventId among ties — exactly the required rule.
# WRONG: front of the list is the lowest priority self.sorted_list.add((priority, event_id)) event_id = self.sorted_list.pop(0)[1] # returns lowest-priority event # CORRECT: negate priority so the highest sits at the front self.sorted_list.add((-priority, event_id)) event_id = self.sorted_list.pop(0)[1] # returns highest-priority event
Tip: If you prefer to keep positive priorities for readability, store
(priority, eventId)and pop from the back usingpop(-1). But beware — popping from the back breaks the tie-breaking rule, because among equal prioritiespop(-1)would return the largesteventId, not the smallest. The negation approach is safer because both ordering and tie-breaking are handled consistently in one direction.
Pitfall 2: Letting the hash map and sorted set fall out of sync
The correctness of updatePriority hinges entirely on the priority_map always reflecting the exact priority currently stored in the SortedList. If you update one structure but forget the other, the remove((-old_priority, eventId)) call will throw a ValueError, because the tuple it tries to delete no longer exists.
Two specific scenarios cause this drift:
-
Updating the map before computing the old tuple. You must read
old_priorityfrom the map before overwriting it. Reversing the order corrupts the value used to locate the stale tuple.# WRONG: overwrites old_priority before using it for removal self.priority_map[eventId] = newPriority old_priority = self.priority_map[eventId] # now equals newPriority! self.sorted_list.remove((-old_priority, eventId)) # ValueError # CORRECT: read old value first, remove, then update old_priority = self.priority_map[eventId] self.sorted_list.remove((-old_priority, eventId)) self.sorted_list.add((-newPriority, eventId)) self.priority_map[eventId] = newPriority -
Forgetting to remove the polled event from the map in
pollHighest. If you onlypop(0)from theSortedListbut leave the entry inpriority_map, a later (illegal but possible in buggy test harnesses)updatePriorityon that stale id would try to remove a tuple that is no longer in the list, again raisingValueError. Always keep both structures consistent:event_id = self.sorted_list.pop(0)[1] self.priority_map.pop(event_id) # keep map aligned with active events
Pitfall 3: Assuming updatePriority always increases the priority
The problem never guarantees that newPriority > old_priority. A priority may be lowered, which could move an event from the front toward the back of the ordering. The remove-then-add strategy handles both increases and decreases uniformly, so resist the temptation to "optimize" by skipping the removal when you assume the new value is larger — that shortcut produces a corrupted set.
Pitfall 4: Misusing SortedList.pop(0) complexity expectations
SortedList.pop(0) is O(log n) here, which is fine. However, a naive alternative implementation using a plain Python list with list.sort() on every operation would degrade to O(n log n) per call. Relying on a heap (heapq) instead is tempting, but a standard binary heap does not support efficient arbitrary removal needed by updatePriority; you would need lazy deletion with a "stale entry" marker. The SortedList cleanly avoids this complication by supporting O(log n) targeted removal.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapConsider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
https assets algo monster cover_photos heap svg Priority Queue and Heap If you'd prefer a video div class responsive iframe iframe width 560 height 315 src https www youtube nocookie com embed QSLbp6NO2 s title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!