1353. Maximum Number of Events That Can Be Attended
Problem Description
You have a list of events, where each event is represented by a start day and an end day. Each event i
is given as [startDay_i, endDay_i]
, meaning the event starts on startDay_i
and ends on endDay_i
.
You can attend an event on any day during its duration - that is, you can attend event i
on any day d
where startDay_i ≤ d ≤ endDay_i
. However, there's an important constraint: you can only attend one event per day.
Your goal is to find the maximum number of events you can attend.
For example, if you have events [[1,2], [2,3], [3,4]]
:
- Event 1 runs from day 1 to day 2
- Event 2 runs from day 2 to day 3
- Event 3 runs from day 3 to day 4
You could attend all 3 events by:
- Attending event 1 on day 1
- Attending event 2 on day 2
- Attending event 3 on day 3
The key insight is that for each day, if multiple events are available, you should greedily choose to attend the event that will end soonest. This maximizes your future options for attending other events.
Intuition
The key observation is that we need to make optimal decisions about which event to attend on each day. If multiple events are available on a given day, which one should we choose?
Consider this scenario: on day 2, we have two events available - one ending on day 3 and another ending on day 7. If we choose the event ending on day 7, we might miss the opportunity to attend the event ending on day 3 (since it will expire soon). However, if we choose the event ending on day 3, we still have more days to attend the other event.
This leads us to a greedy strategy: always attend the event that will end soonest. By doing this, we minimize the risk of missing events that are about to expire.
To implement this strategy efficiently, we need to:
- Process days in chronological order
- Keep track of which events become available on each day
- Among all available events on a given day, pick the one with the earliest end time
A priority queue (min-heap) is perfect for this - it allows us to efficiently track all currently available events sorted by their end times. As we move through each day:
- We add any events that start on that day to our pool of available events
- We remove any events that have already expired (ended before the current day)
- We attend the event with the earliest end time from our available pool
This greedy approach ensures we maximize the number of events attended because we're always prioritizing events that have less flexibility in when they can be attended.
Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.
Solution Approach
The implementation uses a hash table combined with a greedy algorithm and a priority queue to solve this problem efficiently.
Step 1: Organize Events by Start Time
First, we create a hash table g
where the key is the start day and the value is a list of end days for all events starting on that day. We also track the minimum start day l
and maximum end day r
across all events:
g = defaultdict(list)
l, r = inf, 0
for s, e in events:
g[s].append(e)
l = min(l, s)
r = max(r, e)
This preprocessing allows us to quickly access all events that become available on any given day.
Step 2: Process Each Day Sequentially
We iterate through each day from l
to r
and maintain a min-heap pq
that stores the end times of all currently available events:
pq = []
ans = 0
for s in range(l, r + 1):
Step 3: Remove Expired Events
For each day s
, we first remove all events that have already expired (their end time is before the current day):
while pq and pq[0] < s: heappop(pq)
Step 4: Add Newly Available Events
We add all events that start on the current day s
to our priority queue:
for e in g[s]: heappush(pq, e)
Step 5: Attend the Best Event
If there are any available events, we attend the one with the earliest end time (the minimum element in our heap) and increment our answer:
if pq: heappop(pq) ans += 1
By removing this event from the queue, we ensure we don't try to attend it again on a future day.
The algorithm ensures that at each day, we make the optimal greedy choice by attending the event that will expire soonest, maximizing the total number of events we can attend. The time complexity is O(n log n)
where n
is the total number of days in the range, and the space complexity is O(m)
where m
is the number of events.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a concrete example: events = [[1,4], [4,4], [2,2], [3,4], [1,1]]
Step 1: Organize Events by Start Time
First, we create our hash table g
and find the range of days:
- Event
[1,4]
: starts day 1, ends day 4 - Event
[4,4]
: starts day 4, ends day 4 - Event
[2,2]
: starts day 2, ends day 2 - Event
[3,4]
: starts day 3, ends day 4 - Event
[1,1]
: starts day 1, ends day 1
Our hash table g
becomes:
g = { 1: [4, 1], # Two events start on day 1 2: [2], # One event starts on day 2 3: [4], # One event starts on day 3 4: [4] # One event starts on day 4 }
Range: l = 1
, r = 4
Step 2-5: Process Each Day
Day 1:
- Priority queue (min-heap) starts empty:
pq = []
- No expired events to remove
- Add events starting on day 1:
pq = [1, 4]
(sorted by end time) - Attend event ending on day 1:
heappop(pq)
removes 1 pq = [4]
,ans = 1
Day 2:
- Current
pq = [4]
- Remove expired events: 4 ≥ 2, so nothing removed
- Add events starting on day 2:
heappush(pq, 2)
pq = [2, 4]
(heap maintains min property)- Attend event ending on day 2:
heappop(pq)
removes 2 pq = [4]
,ans = 2
Day 3:
- Current
pq = [4]
- Remove expired events: 4 ≥ 3, so nothing removed
- Add events starting on day 3:
heappush(pq, 4)
pq = [4, 4]
(two events both ending on day 4)- Attend one event ending on day 4:
heappop(pq)
removes one 4 pq = [4]
,ans = 3
Day 4:
- Current
pq = [4]
- Remove expired events: 4 ≥ 4, so nothing removed
- Add events starting on day 4:
heappush(pq, 4)
pq = [4, 4]
- Attend one event ending on day 4:
heappop(pq)
removes one 4 pq = [4]
,ans = 4
Final Result: We attended 4 events total.
The greedy strategy worked perfectly here - by always choosing the event that ends soonest, we were able to attend:
- Day 1: Event
[1,1]
- Day 2: Event
[2,2]
- Day 3: Event
[3,4]
or[1,4]
- Day 4: Event
[4,4]
Solution Implementation
1from collections import defaultdict
2from heapq import heappush, heappop
3from typing import List
4
5class Solution:
6 def maxEvents(self, events: List[List[int]]) -> int:
7 # Group events by their start day
8 events_by_start_day = defaultdict(list)
9
10 # Find the range of days we need to consider
11 min_start_day = float('inf')
12 max_end_day = 0
13
14 for start_day, end_day in events:
15 events_by_start_day[start_day].append(end_day)
16 min_start_day = min(min_start_day, start_day)
17 max_end_day = max(max_end_day, end_day)
18
19 # Min heap to store end days of available events
20 available_events = []
21 attended_events = 0
22
23 # Process each day in the range
24 for current_day in range(min_start_day, max_end_day + 1):
25 # Remove events that have already ended (end day < current day)
26 while available_events and available_events[0] < current_day:
27 heappop(available_events)
28
29 # Add all events starting on the current day to the heap
30 # Store their end days in the heap
31 for end_day in events_by_start_day[current_day]:
32 heappush(available_events, end_day)
33
34 # Attend the event that ends earliest (greedy approach)
35 if available_events:
36 heappop(available_events)
37 attended_events += 1
38
39 return attended_events
40
1class Solution {
2 public int maxEvents(int[][] events) {
3 // Map to store events grouped by their start day
4 // Key: start day, Value: list of end days for events starting on that day
5 Map<Integer, List<Integer>> startDayToEndDays = new HashMap<>();
6
7 // Track the overall time range of all events
8 int minStartDay = Integer.MAX_VALUE;
9 int maxEndDay = 0;
10
11 // Group events by their start day and find the time range
12 for (int[] event : events) {
13 int startDay = event[0];
14 int endDay = event[1];
15
16 // Add this event's end day to the list of events starting on startDay
17 startDayToEndDays.computeIfAbsent(startDay, k -> new ArrayList<>()).add(endDay);
18
19 // Update the overall time range
20 minStartDay = Math.min(minStartDay, startDay);
21 maxEndDay = Math.max(maxEndDay, endDay);
22 }
23
24 // Min heap to store end days of available events (sorted by earliest end day)
25 PriorityQueue<Integer> availableEvents = new PriorityQueue<>();
26
27 // Counter for maximum number of events we can attend
28 int maxAttendableEvents = 0;
29
30 // Process each day in the time range
31 for (int currentDay = minStartDay; currentDay <= maxEndDay; currentDay++) {
32 // Remove events that have already ended (end day < current day)
33 while (!availableEvents.isEmpty() && availableEvents.peek() < currentDay) {
34 availableEvents.poll();
35 }
36
37 // Add all events that start on the current day to available events
38 for (int endDay : startDayToEndDays.getOrDefault(currentDay, List.of())) {
39 availableEvents.offer(endDay);
40 }
41
42 // Attend one event on this day (choose the one ending earliest)
43 if (!availableEvents.isEmpty()) {
44 availableEvents.poll();
45 maxAttendableEvents++;
46 }
47 }
48
49 return maxAttendableEvents;
50 }
51}
52
1class Solution {
2public:
3 int maxEvents(vector<vector<int>>& events) {
4 // Map to store events grouped by their start day
5 // Key: start day, Value: list of end days for events starting on this day
6 unordered_map<int, vector<int>> startDayToEndDays;
7
8 // Track the range of days we need to process
9 int minStartDay = INT_MAX;
10 int maxEndDay = 0;
11
12 // Group events by start day and find the overall time range
13 for (auto& event : events) {
14 int startDay = event[0];
15 int endDay = event[1];
16
17 startDayToEndDays[startDay].push_back(endDay);
18 minStartDay = min(minStartDay, startDay);
19 maxEndDay = max(maxEndDay, endDay);
20 }
21
22 // Min-heap to store end days of available events
23 // We always want to attend the event that ends earliest
24 priority_queue<int, vector<int>, greater<int>> minHeap;
25
26 int attendedEvents = 0;
27
28 // Process each day in the range
29 for (int currentDay = minStartDay; currentDay <= maxEndDay; ++currentDay) {
30 // Remove events that have already ended (end day < current day)
31 while (!minHeap.empty() && minHeap.top() < currentDay) {
32 minHeap.pop();
33 }
34
35 // Add all events that start on the current day to the heap
36 for (int endDay : startDayToEndDays[currentDay]) {
37 minHeap.push(endDay);
38 }
39
40 // Attend one event on this day (choose the one ending earliest)
41 if (!minHeap.empty()) {
42 minHeap.pop();
43 ++attendedEvents;
44 }
45 }
46
47 return attendedEvents;
48 }
49};
50
1/**
2 * Finds the maximum number of events that can be attended
3 * Each event can only be attended for one day
4 * @param events - Array of events where each event is [startDay, endDay]
5 * @returns Maximum number of events that can be attended
6 */
7function maxEvents(events: number[][]): number {
8 // Map to group events by their start day
9 // Key: start day, Value: array of end days for events starting on this day
10 const eventsByStartDay: Map<number, number[]> = new Map();
11
12 // Track the earliest start day and latest end day across all events
13 let earliestStartDay = Infinity;
14 let latestEndDay = 0;
15
16 // Process each event and build the map
17 for (const [startDay, endDay] of events) {
18 // Initialize array for this start day if not exists
19 if (!eventsByStartDay.has(startDay)) {
20 eventsByStartDay.set(startDay, []);
21 }
22
23 // Add the end day to the list of events starting on this day
24 eventsByStartDay.get(startDay)!.push(endDay);
25
26 // Update the overall time range
27 earliestStartDay = Math.min(earliestStartDay, startDay);
28 latestEndDay = Math.max(latestEndDay, endDay);
29 }
30
31 // Min heap to store end days of available events
32 // Always attend the event that ends earliest
33 const minHeap = new MinPriorityQueue<number>();
34
35 // Count of events attended
36 let attendedEventsCount = 0;
37
38 // Iterate through each day in the time range
39 for (let currentDay = earliestStartDay; currentDay <= latestEndDay; currentDay++) {
40 // Remove events that have already ended (before current day)
41 while (!minHeap.isEmpty() && minHeap.front() < currentDay) {
42 minHeap.dequeue();
43 }
44
45 // Add all events starting on the current day to the heap
46 for (const endDay of eventsByStartDay.get(currentDay) || []) {
47 minHeap.enqueue(endDay);
48 }
49
50 // Attend one event on this day (the one ending earliest)
51 if (!minHeap.isEmpty()) {
52 minHeap.dequeue();
53 attendedEventsCount++;
54 }
55 }
56
57 return attendedEventsCount;
58}
59
Time and Space Complexity
Time Complexity: O(M × log n)
, where M
is the range of days from the minimum start day to the maximum end day of all events, and n
is the number of events.
- The initial loop to build the graph and find min/max days takes
O(n)
time - The main loop iterates through each day from
l
tor
, which isO(M)
iterations - Within each day:
- The while loop for removing expired events performs at most
n
total heap pops across all iterations, each takingO(log n)
time - Adding events that start on day
s
involves heap push operations, with at mostn
total pushes across all days, each takingO(log n)
time - Attending an event (if available) involves one heap pop, taking
O(log n)
time
- The while loop for removing expired events performs at most
- Overall:
O(M)
iterations ×O(log n)
operations per iteration =O(M × log n)
Space Complexity: O(n)
, where n
is the number of events.
- The dictionary
g
stores all events grouped by start day, usingO(n)
space - The priority queue
pq
can contain at mostn
events at any given time, usingO(n)
space - Other variables use
O(1)
space - Total space complexity:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient Day Range Iteration for Sparse Events
The current solution iterates through every single day from min_start_day
to max_end_day
. When events are sparse (e.g., events at days 1 and 100000 only), this creates unnecessary iterations through empty days, leading to poor performance.
Example problematic case:
events = [[1, 1], [100000, 100000]] # The algorithm iterates through 99,999 days unnecessarily
Solution: Instead of iterating through all days, only process days where events actually start or could be attended:
def maxEvents(self, events: List[List[int]]) -> int:
events.sort() # Sort by start day
available_events = []
attended_events = 0
day = 0
i = 0
while i < len(events) or available_events:
# If no available events, jump to next event's start day
if not available_events:
day = events[i][0]
# Add all events starting on current day
while i < len(events) and events[i][0] <= day:
heappush(available_events, events[i][1])
i += 1
# Remove expired events
while available_events and available_events[0] < day:
heappop(available_events)
# Attend event ending soonest
if available_events:
heappop(available_events)
attended_events += 1
day += 1
return attended_events
2. Memory Overhead from Dictionary Storage
Using defaultdict(list)
to group events by start day can consume significant memory when there are many unique start days. Additionally, accessing dictionary keys has overhead compared to sorted array traversal.
Solution: Sort the events array directly and process them in order, eliminating the need for the dictionary:
events.sort(key=lambda x: x[0]) # Sort by start day # Process events sequentially without dictionary
3. Not Handling Edge Cases with Overlapping Single-Day Events
When multiple events occur on the same single day (e.g., [[1,1], [1,1], [1,1]]
), the algorithm correctly handles this by only attending one. However, developers might incorrectly assume they need special handling for this case, leading to overly complex code.
Key insight: The existing greedy approach naturally handles this case - only one event can be popped from the heap per day, regardless of how many single-day events exist on that day.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!