Facebook Pixel

3885. Design Event Manager

MediumDesignArrayHash TableOrdered SetHeap (Priority Queue)
LeetCode ↗

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, where events[i] = [eventId_i, priority_i]. Each eventId is unique.

  • void updatePriority(int eventId, int newPriority): Updates the priority of the active event with id eventId to newPriority. (You can assume the given eventId corresponds to an active event.)

  • int pollHighest(): Removes and returns the eventId of 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 eventId among them.
    • If there are no active events remaining, return -1.

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() returns 2, because event 2 has the highest priority 8. Event 2 is now removed.
  • updatePriority(3, 10) changes the priority of event 3 from 5 to 10.
  • pollHighest() returns 3, since event 3 now has priority 10, the highest among the remaining active events.
  • pollHighest() returns 1, 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.

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

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.

Design adatastructure?yesSimulationorstraightforward?noDesign +Supporting DataStructures

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 Flowchart

Intuition

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 set sl. The negation of priority ensures the highest priority sits at the front, and the eventId as 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 set sl. 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) into sl.
  • 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 sl is empty. If it is, we return -1 because 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 its eventId via sl.pop(0)[1].
  • We delete the event's priority information from the hash map with d.pop(eventId), keeping d consistent with the active events in sl.
  • 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 each eventId to its current priority, enabling updatePriority to locate and remove the old tuple in O(1) lookup time.

Complexity analysis

Let n be the number of active events.

  • Time complexity: Each updatePriority and pollHighest operation performs a constant number of sorted-set insertions, removals, or lookups, each costing O(log n). The hash map operations are O(1). Initialization costs O(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) to sl, set d[1] = 5
  • Event (2, 8) → add (-8, 2) to sl, set d[2] = 8
  • Event (3, 5) → add (-5, 3) to sl, set d[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) from sl — the hash map told us exactly which tuple to delete.
  • Add the new tuple (-10, 3) to sl.
  • 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

OperationActionsl afterResult
initinsert 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()
41
1class 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 */
85
1class 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 */
58
1// 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}
91

Time and Space Complexity

Time Complexity:

  • Initialization (__init__): The constructor iterates over all n initial events. For each event, it performs self.sl.add(...), which takes O(log n) time on a SortedList, and a dictionary insertion, which takes O(1) time. Therefore, the total initialization time is O(n log n).

  • updatePriority: This method performs one self.sl.remove(...) operation and one self.sl.add(...) operation, each taking O(log n) time on a SortedList. The dictionary lookups and updates take O(1) time. Thus, each call to updatePriority takes O(log n) time.

  • pollHighest: This method performs one self.sl.pop(0) operation, which takes O(log n) time on a SortedList, along with O(1) dictionary operations. Therefore, each call to pollHighest takes O(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 using pop(-1). But beware — popping from the back breaks the tie-breaking rule, because among equal priorities pop(-1) would return the largest eventId, 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:

  1. Updating the map before computing the old tuple. You must read old_priority from 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
  2. Forgetting to remove the polled event from the map in pollHighest. If you only pop(0) from the SortedList but leave the entry in priority_map, a later (illegal but possible in buggy test harnesses) updatePriority on that stale id would try to remove a tuple that is no longer in the list, again raising ValueError. 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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More