Facebook Pixel

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.

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

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:

  1. Process days in chronological order
  2. Keep track of which events become available on each day
  3. 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 Evaluator

Example 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 to r, which is O(M) iterations
  • Within each day:
    • The while loop for removing expired events performs at most n total heap pops across all iterations, each taking O(log n) time
    • Adding events that start on day s involves heap push operations, with at most n total pushes across all days, each taking O(log n) time
    • Attending an event (if available) involves one heap pop, taking O(log n) time
  • 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, using O(n) space
  • The priority queue pq can contain at most n events at any given time, using O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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