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.
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:
-
Why sorting helps: Once events are sorted by start time, for any event ending at time
e
, all valid second events must start at timee + 1
or later. Due to the sorting, these valid events form a contiguous suffix in our sorted array. -
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 positioni
to the end, we can build this array in reverse order:f[i] = max(f[i+1], events[i].value)
. -
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 timee
. This is a classic binary search problem - we're looking for the leftmost position wherestartTime > e
. -
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
- Find the index
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 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
wheref[i]
will store the maximum value of any single event from indexi
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 positioni
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 valuev
:- Use
bisect_right
to find the first event whose start time is greater thane
- 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 wherestartTime > 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
- Use
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)
, totalO(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 EvaluatorExample 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.
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 # We search for events where start_time > end_time of current event
21 left, right = 0, n
22 while left < right:
23 mid = (left + right) // 2
24 if events[mid][0] > end:
25 right = mid
26 else:
27 left = mid + 1
28
29 # If there's a valid non-overlapping event, add its maximum possible value
30 current_sum = value
31 if left < n:
32 current_sum += max_value_from[left]
33
34 # Update the maximum sum
35 max_sum = max(max_sum, current_sum)
36
37 return max_sum
38
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 // We're looking for the leftmost event where startTime > currentEndTime
26 int left = 0;
27 int right = n;
28
29 while (left < right) {
30 int mid = (left + right) >> 1;
31
32 if (events[mid][0] > currentEndTime) {
33 // This event doesn't overlap, try to find an earlier one
34 right = mid;
35 } else {
36 // This event overlaps, search in the right half
37 left = mid + 1;
38 }
39 }
40
41 // If we found a non-overlapping event, add its maximum possible value
42 if (left < n) {
43 currentValue += maxValueFromIndex[left];
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 // We're looking for the leftmost event where events[mid][0] > currentEvent[1]
26 int left = 0, right = n;
27 while (left < right) {
28 int mid = (left + right) >> 1;
29 if (events[mid][0] > currentEvent[1]) {
30 // This event doesn't overlap, try to find an earlier one
31 right = mid;
32 } else {
33 // This event overlaps, need to look further right
34 left = mid + 1;
35 }
36 }
37
38 // If we found a non-overlapping event, add its best value to current
39 if (left < n) {
40 currentValue += maxValueFromIndex[left];
41 }
42
43 // Update the maximum sum found so far
44 maxSum = max(maxSum, currentValue);
45 }
46
47 return maxSum;
48 }
49};
50
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 let left: number = 0;
27 let right: number = eventCount;
28
29 while (left < right) {
30 const mid: number = (left + right) >> 1;
31
32 if (events[mid][0] > endTime) {
33 // Event at mid starts after current event ends, search left half
34 right = mid;
35 } else {
36 // Event at mid overlaps with current event, search right half
37 left = mid + 1;
38 }
39 }
40
41 // Get the maximum value from events that start after current event ends
42 const maxValueAfter: number = left < eventCount ? suffixMax[left] : 0;
43
44 // Update maximum sum considering current event value + best non-overlapping event
45 maxSum = Math.max(maxSum, maxValueAfter + value);
46 }
47
48 return maxSum;
49}
50
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is dominated by:
- Sorting the events array:
O(n × log n)
- Building the suffix maximum array
f
:O(n)
- single pass through the array - For each event in the main loop (
n
iterations):bisect_right
operation with custom key:O(log n)
binary search- Other operations:
O(1)
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:
- The suffix maximum array
f
:O(n)
- storesn
elements - The sorting operation (Python's Timsort):
O(n)
in worst case - 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. 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 left, right = 0, n while left < right: mid = (left + right) // 2 if events[mid][0] >= end: # Wrong comparison! right = mid else: left = mid + 1
Correct Implementation:
# CORRECT: Find events where start_time > end_time left, right = 0, n while left < right: mid = (left + right) // 2 if events[mid][0] > end: # Strict inequality right = mid else: left = mid + 1
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 insertend
to keep the list sorted by start times, which gives us the first event withstart_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]
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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
Want a Structured Path to Master System Design Too? Don’t Miss This!