Facebook Pixel

2054. Two Best Non-Overlapping Events

Problem Description

You are given a list of events where each event has a start time, end time, and value. Each event is represented as [startTime, endTime, value]. Your task is to select at most two non-overlapping events such that the sum of their values is maximized.

Two events are considered overlapping if one event's end time is the same as or after another event's start time. In other words, if you attend an event that ends at time t, the next event you can attend must start at time t + 1 or later.

The goal is to find the maximum possible sum of values you can obtain by attending at most two events. You can choose to attend:

  • No events (sum = 0)
  • One event (sum = value of that event)
  • Two non-overlapping events (sum = value of first event + value of second event)

For example, if you have events [[1,3,4], [3,4,3], [5,6,5]]:

  • You cannot attend events [1,3,4] and [3,4,3] together because the first ends at 3 and the second starts at 3 (they overlap)
  • You can attend events [1,3,4] and [5,6,5] together for a total value of 9
  • You can attend events [3,4,3] and [5,6,5] together for a total value of 8

The function should return the maximum sum achievable, which in this case would be 9.

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

Intuition

The key insight is that we need to consider each event as a potential "first event" and then find the best possible "second event" that doesn't overlap with it.

If we sort the events by their start times, we can leverage the ordering to efficiently find non-overlapping events. For any event we choose as the first event, we need to find events that start after this event ends.

Here's the thought process:

  1. Why sorting helps: Once events are sorted by start time, for any event ending at time e, all valid second events must start at time e + 1 or later. Due to the sorting, these valid events form a contiguous suffix in our sorted array.

  2. Precomputation optimization: Instead of checking all possible second events for each first event, we can precompute the maximum value available from each position onwards. If f[i] represents the maximum value of any single event from position i to the end, we can build this array in reverse order: f[i] = max(f[i+1], events[i].value).

  3. Binary search for efficiency: For each event ending at time e, we need to find the first event in our sorted array that starts after time e. This is a classic binary search problem - we're looking for the leftmost position where startTime > e.

  4. Combining the pieces: For each event with value v:

    • Find the index idx of the first non-overlapping event using binary search
    • If such events exist, the best total value is v + f[idx] (current event's value plus the best value from non-overlapping events)
    • If no non-overlapping events exist, the value is just v
    • Track the maximum across all possibilities

This approach ensures we consider all valid combinations while avoiding the brute force O(n²) comparison of all pairs.

Learn more about Binary Search, Dynamic Programming, Sorting and Heap (Priority Queue) patterns.

Solution Implementation

1class Solution:
2    def maxTwoEvents(self, events: List[List[int]]) -> int:
3        # Sort events by start time
4        events.sort()
5        n = len(events)
6
7        # max_value_from[i] stores the maximum value from event i to the last event
8        max_value_from = [0] * n
9        max_value_from[-1] = events[-1][2]
10
11        # Build the max_value_from array from right to left
12        for i in range(n - 2, -1, -1):
13            max_value_from[i] = max(max_value_from[i + 1], events[i][2])
14
15        max_sum = 0
16
17        # For each event, find the best non-overlapping event that starts after it ends
18        for start, end, value in events:
19            # Binary search for the first event that starts after current event ends
20            # feasible(mid) = events[mid][0] > end (start time > current end time)
21            # Pattern: [false, false, ..., true, true, true]
22            left, right = 0, n - 1
23            first_true_index = -1
24
25            while left <= right:
26                mid = (left + right) // 2
27                if events[mid][0] > end:  # feasible condition
28                    first_true_index = mid
29                    right = mid - 1
30                else:
31                    left = mid + 1
32
33            # If there's a valid non-overlapping event, add its maximum possible value
34            current_sum = value
35            if first_true_index != -1:
36                current_sum += max_value_from[first_true_index]
37
38            # Update the maximum sum
39            max_sum = max(max_sum, current_sum)
40
41        return max_sum
42
1class Solution {
2    public int maxTwoEvents(int[][] events) {
3        // Sort events by start time in ascending order
4        Arrays.sort(events, (a, b) -> a[0] - b[0]);
5
6        int n = events.length;
7
8        // maxValueFromIndex[i] stores the maximum value among all events starting from index i
9        int[] maxValueFromIndex = new int[n + 1];
10
11        // Build suffix maximum array from right to left
12        // maxValueFromIndex[i] = max value of all events from index i to n-1
13        for (int i = n - 1; i >= 0; i--) {
14            maxValueFromIndex[i] = Math.max(maxValueFromIndex[i + 1], events[i][2]);
15        }
16
17        int maxSum = 0;
18
19        // For each event, find the best non-overlapping event to pair with
20        for (int[] currentEvent : events) {
21            int currentValue = currentEvent[2];
22            int currentEndTime = currentEvent[1];
23
24            // Binary search to find the first event that starts after current event ends
25            // feasible(mid) = events[mid][0] > currentEndTime
26            // Pattern: [false, false, ..., true, true, true]
27            int left = 0;
28            int right = n - 1;
29            int firstTrueIndex = -1;
30
31            while (left <= right) {
32                int mid = left + (right - left) / 2;
33                if (events[mid][0] > currentEndTime) {
34                    firstTrueIndex = mid;
35                    right = mid - 1;
36                } else {
37                    left = mid + 1;
38                }
39            }
40
41            // If we found a non-overlapping event, add its maximum possible value
42            if (firstTrueIndex != -1) {
43                currentValue += maxValueFromIndex[firstTrueIndex];
44            }
45
46            // Update the maximum sum found so far
47            maxSum = Math.max(maxSum, currentValue);
48        }
49
50        return maxSum;
51    }
52}
53
1class Solution {
2public:
3    int maxTwoEvents(vector<vector<int>>& events) {
4        // Sort events by start time
5        ranges::sort(events);
6
7        int n = events.size();
8
9        // maxValueFromIndex[i] stores the maximum value among all events from index i to n-1
10        vector<int> maxValueFromIndex(n + 1);
11
12        // Build the suffix maximum array from right to left
13        // maxValueFromIndex[i] = max(event[i].value, event[i+1].value, ..., event[n-1].value)
14        for (int i = n - 1; i >= 0; --i) {
15            maxValueFromIndex[i] = max(maxValueFromIndex[i + 1], events[i][2]);
16        }
17
18        int maxSum = 0;
19
20        // For each event, try to find the best non-overlapping event to pair with
21        for (const auto& currentEvent : events) {
22            int currentValue = currentEvent[2];
23
24            // Binary search to find the first event that starts after current event ends
25            // feasible(mid) = events[mid][0] > currentEvent[1]
26            // Pattern: [false, false, ..., true, true, true]
27            int left = 0;
28            int right = n - 1;
29            int firstTrueIndex = -1;
30
31            while (left <= right) {
32                int mid = left + (right - left) / 2;
33                if (events[mid][0] > currentEvent[1]) {
34                    firstTrueIndex = mid;
35                    right = mid - 1;
36                } else {
37                    left = mid + 1;
38                }
39            }
40
41            // If we found a non-overlapping event, add its best value to current
42            if (firstTrueIndex != -1) {
43                currentValue += maxValueFromIndex[firstTrueIndex];
44            }
45
46            // Update the maximum sum found so far
47            maxSum = max(maxSum, currentValue);
48        }
49
50        return maxSum;
51    }
52};
53
1/**
2 * Finds the maximum sum of values from at most two non-overlapping events
3 * @param events - Array of events where each event is [startTime, endTime, value]
4 * @returns Maximum sum of values from at most two non-overlapping events
5 */
6function maxTwoEvents(events: number[][]): number {
7    // Sort events by start time in ascending order
8    events.sort((a, b) => a[0] - b[0]);
9
10    const eventCount: number = events.length;
11
12    // Create suffix maximum array to store the maximum value from index i to end
13    // suffixMax[i] represents the maximum value among events[i], events[i+1], ..., events[n-1]
14    const suffixMax: number[] = Array(eventCount + 1).fill(0);
15
16    // Build suffix maximum array from right to left
17    for (let i = eventCount - 1; i >= 0; i--) {
18        suffixMax[i] = Math.max(suffixMax[i + 1], events[i][2]);
19    }
20
21    let maxSum: number = 0;
22
23    // For each event, find the best non-overlapping event that starts after it ends
24    for (const [startTime, endTime, value] of events) {
25        // Binary search to find the first event that starts after current event ends
26        // feasible(mid) = events[mid][0] > endTime
27        // Pattern: [false, false, ..., true, true, true]
28        let left: number = 0;
29        let right: number = eventCount - 1;
30        let firstTrueIndex: number = -1;
31
32        while (left <= right) {
33            const mid: number = Math.floor((left + right) / 2);
34            if (events[mid][0] > endTime) {
35                firstTrueIndex = mid;
36                right = mid - 1;
37            } else {
38                left = mid + 1;
39            }
40        }
41
42        // Get the maximum value from events that start after current event ends
43        const maxValueAfter: number = firstTrueIndex !== -1 ? suffixMax[firstTrueIndex] : 0;
44
45        // Update maximum sum considering current event value + best non-overlapping event
46        maxSum = Math.max(maxSum, maxValueAfter + value);
47    }
48
49    return maxSum;
50}
51

Solution Approach

The solution implements the sorting and binary search strategy with precomputation:

Step 1: Sort the events

events.sort()

Events are sorted by start time (first element of each sublist by default). This allows us to use binary search later.

Step 2: Precompute maximum values from each position

n = len(events)
f = [events[-1][2]] * n
for i in range(n - 2, -1, -1):
    f[i] = max(f[i + 1], events[i][2])
  • Initialize array f where f[i] will store the maximum value of any single event from index i to the end
  • Start with the last event's value for all positions
  • Iterate backwards from the second-to-last event, updating f[i] = max(f[i+1], events[i][2])
  • This ensures f[i] contains the maximum value achievable from position i onwards

Step 3: Process each event and find the best combination

ans = 0
for _, e, v in events:
    idx = bisect_right(events, e, key=lambda x: x[0])
    if idx < n:
        v += f[idx]
    ans = max(ans, v)
  • For each event with end time e and value v:
    • Use bisect_right to find the first event whose start time is greater than e
    • The key=lambda x: x[0] tells bisect to compare only start times
    • bisect_right(events, e, key=lambda x: x[0]) finds the leftmost position where startTime > e
    • If such an event exists (idx < n), add the best value from that position (f[idx]) to the current event's value
    • Update the answer with the maximum value found

Time Complexity: O(n log n) where n is the number of events

  • Sorting: O(n log n)
  • Precomputation: O(n)
  • For each event, binary search: O(log n), total O(n log n)

Space Complexity: O(n) for the precomputed array f

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 small example: events = [[1,3,4], [3,4,3], [5,6,5]]

Step 1: Sort the events by start time After sorting: [[1,3,4], [3,4,3], [5,6,5]] (already sorted in this case)

  • Index 0: [1,3,4] - starts at 1, ends at 3, value = 4
  • Index 1: [3,4,3] - starts at 3, ends at 4, value = 3
  • Index 2: [5,6,5] - starts at 5, ends at 6, value = 5

Step 2: Build the precomputed array f We build f from right to left, where f[i] = maximum value from index i to the end:

  • f[2] = 5 (only event at index 2, value = 5)
  • f[1] = max(f[2], events[1][2]) = max(5, 3) = 5 (best from index 1 onwards is 5)
  • f[0] = max(f[1], events[0][2]) = max(5, 4) = 5 (best from index 0 onwards is 5)

Result: f = [5, 5, 5]

Step 3: Process each event and find best combinations

Processing event [1,3,4] (index 0):

  • End time = 3, value = 4
  • Binary search: Find first event starting after time 3
    • bisect_right with key on start times finds index 2 (event [5,6,5] starts at 5 > 3)
  • Total value = 4 + f[2] = 4 + 5 = 9
  • Update answer: ans = max(0, 9) = 9

Processing event [3,4,3] (index 1):

  • End time = 4, value = 3
  • Binary search: Find first event starting after time 4
    • bisect_right finds index 2 (event [5,6,5] starts at 5 > 4)
  • Total value = 3 + f[2] = 3 + 5 = 8
  • Update answer: ans = max(9, 8) = 9

Processing event [5,6,5] (index 2):

  • End time = 6, value = 5
  • Binary search: Find first event starting after time 6
    • bisect_right finds index 3 (no events after, idx = 3 = n)
  • Since idx >= n, no valid second event exists
  • Total value = 5 (just this event)
  • Update answer: ans = max(9, 5) = 9

Final Answer: 9

The maximum value is achieved by attending events [1,3,4] and [5,6,5], which don't overlap and give a total value of 4 + 5 = 9.

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity is dominated by:

  1. Sorting the events array: O(n × log n)

  2. Building the suffix maximum array f: O(n) - single pass through the array

  3. For each event in the main loop (n iterations):

    • bisect_right operation with custom key: O(log n) binary search
    • Other operations: O(1)

    Total for this loop: O(n × log n)

Overall time complexity: O(n × log n) + O(n) + O(n × log n) = O(n × log n)

Space Complexity: O(n)

The space complexity consists of:

  1. The suffix maximum array f: O(n) - stores n elements
  2. The sorting operation (Python's Timsort): O(n) in worst case
  3. Other variables (ans, idx, loop variables): O(1)

Overall space complexity: O(n)

Where n is the number of events in the input array.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using Wrong Binary Search Template Variant

A common pitfall is using a different binary search variant that doesn't match the canonical template. The correct template uses while left <= right with first_true_index tracking.

Incorrect Implementation:

# WRONG: Using while left < right with right = mid
left, right = 0, n
while left < right:
    mid = (left + right) // 2
    if events[mid][0] > end:
        right = mid
    else:
        left = mid + 1
return left

Correct Implementation:

# CORRECT: Using template with first_true_index
left, right = 0, n - 1
first_true_index = -1
while left <= right:
    mid = (left + right) // 2
    if events[mid][0] > end:  # feasible condition
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
# Use first_true_index (-1 if not found)

2. Incorrect Overlap Definition

A critical pitfall is misunderstanding when two events overlap. Many developers incorrectly assume that if one event ends at time t and another starts at time t, they don't overlap. However, according to the problem, these events DO overlap - you need the next event to start at t + 1 or later.

Incorrect Implementation:

# WRONG: This would find events where start_time >= end_time
if events[mid][0] >= end:  # Wrong comparison - should be strict >

Correct Implementation:

# CORRECT: Find events where start_time > end_time
if events[mid][0] > end:  # Strict inequality

2. Not Considering Single Event as Answer

Another pitfall is forgetting that attending just one high-value event might be better than attending two lower-value events. The precomputed array handles this, but if you try to optimize and only look for pairs, you'll miss cases where a single event has the maximum value.

Example: Events: [[1,2,10], [4,5,1], [6,7,1]]

  • Best two events: value = 10 + 1 = 11
  • But if we had [[1,2,20], [4,5,1], [6,7,1]], the single event with value 20 is better than any pair

Solution: The code correctly handles this by initializing current_sum = value (considering the single event) before potentially adding a second event.

3. Off-by-One Error in Precomputation

When building the max_value_from array, starting from the wrong index or using wrong boundaries can cause incorrect results.

Incorrect:

# WRONG: Missing the last event's value
max_value_from = [0] * n
for i in range(n - 2, -1, -1):
    max_value_from[i] = max(max_value_from[i + 1], events[i][2])

Correct:

# CORRECT: Initialize last element first
max_value_from = [0] * n
max_value_from[-1] = events[-1][2]  # Don't forget this!
for i in range(n - 2, -1, -1):
    max_value_from[i] = max(max_value_from[i + 1], events[i][2])

4. Using bisect_left Instead of Custom Binary Search

Using bisect_left or bisect_right without proper understanding can lead to wrong results. The problem requires finding the first event where start_time > end_time, which needs careful implementation.

Why the manual binary search is clearer:

  • It explicitly shows we're looking for events[mid][0] > end
  • The condition is unambiguous
  • Using bisect_right(events, end, key=lambda x: x[0]) would find where to insert end to keep the list sorted by start times, which gives us the first event with start_time > end

5. Not Handling Edge Cases

Forgetting to check if a valid second event exists before accessing the precomputed array.

Incorrect:

# WRONG: May cause index out of bounds
current_sum = value + max_value_from[left]

Correct:

# CORRECT: Check bounds first
current_sum = value
if left < n:
    current_sum += max_value_from[left]
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

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

Load More