#100 📆 2054. Two Best Non-Overlapping Events 🚀🧠
When juggling multiple opportunities, the trick lies in choosing the ones that offer the highest rewards while avoiding overlaps. This problem is a classic example of finding an optimal solution under constraints, where we aim to maximize the sum of values by attending at most two non-overlapping events. 💡 Let’s unravel it step by step and dive into the magic of efficient algorithms! 🚀
💼 Problem Statement
You are given a 0-indexed 2D integer array events
where:
events[i] = [startTimeᵢ, endTimeᵢ, valueᵢ]
,startTimeᵢ
is the start time of the i-th event,endTimeᵢ
is the end time of the i-th event,valueᵢ
is the value obtained from attending the i-th event.
Goal 🎯
Select at most two non-overlapping events such that the sum of their values is maximized.
Key Constraints:
- Start and end times are inclusive, meaning you cannot attend two events where one starts as the other ends. Specifically:
- If an event ends at time
t
, the next event must start at or aftert + 1
.
- If an event ends at time
- Return the maximum sum of values from two non-overlapping events.
✨ Example 1:
Input:
events = [[1,3,2],[4,5,2],[2,4,3]]
Output:
4
Explanation:
Choose events [1,3,2]
and [4,5,2]
for a total value of 2 + 2 = 4
.
✨ Example 2:
Input:
events = [[1,3,2],[4,5,2],[1,5,5]]
Output:
5
Explanation:
Choose the single event [1,5,5]
since attending any other event would cause overlap.
✨ Example 3:
Input:
events = [[1,5,3],[1,5,1],[6,6,5]]
Output:
8
Explanation:
Choose events [1,5,3]
and [6,6,5]
for a total value of 3 + 5 = 8
.
🧩 Edge Cases to Consider
- Small Inputs
- Only two events that overlap:
events = [[1,2,3],[2,3,5]] Output: 5
- Only one event:
events = [[1,5,10]] Output: 10
- Only two events that overlap:
- All Events Overlap
- Example:
events = [[1,5,2],[2,6,3],[3,7,4]] Output: 4
In this case, you can pick only one event with the highest value.
- Example:
- Disjoint Events
- Example:
events = [[1,2,10],[3,4,20],[5,6,30]] Output: 50
Here, you can pick the two highest-value events as they don’t overlap.
- Example:
- Sparse Time Range
- Events that are widely spaced apart:
events = [[1,2,1],[1000,1001,2],[2000,2001,3]] Output: 5
- Events that are widely spaced apart:
🔍 Key Insights
-
Sorting for Simplicity
Sorting events by their end times ensures that we process events in a timeline order. This makes it easier to compare overlapping conditions. -
Binary Search for Efficiency
When checking for the highest value of a previously attended event that doesn’t overlap with the current event, a binary search helps find the best match quickly. -
Dynamic Programming for Optimization
By maintaining a rolling record of maximum event values, we avoid recomputation and make the solution efficient.
🛠️ Optimized Solution
📚 Detailed Solution Approach
The goal is to maximize the sum of values from at most two non-overlapping events. The key challenges here are:
- Efficiently checking which events can be combined without overlapping.
- Tracking the highest values of possible event combinations as we iterate through the list.
Let’s break down the solution step by step.
🔑 Step 1: Sort Events by End Time
Sorting events by their end times simplifies the problem. After sorting:
- Any previously processed event is guaranteed to end before the current event.
- This ensures that we only check whether the current event overlaps with earlier events and not vice versa.
For example, if the input events are:
events = [[1, 3, 5], [2, 5, 7], [4, 6, 9]]
After sorting by end time (events[i][1]
):
sorted_events = [[1, 3, 5], [2, 5, 7], [4, 6, 9]]
Sorting takes O(n log n) time, where n
is the number of events.
🔑 Step 2: Use Binary Search to Find Compatible Events
For a given event, we need to find the latest event that does not overlap with it.
- Specifically, we want an event whose end time is less than the start time of the current event.
- To do this efficiently, we use binary search on a list of previously processed event end times.
Why Binary Search?
Binary search reduces the complexity of finding compatible events to O(log n) per query.
- Instead of scanning through all prior events to check for compatibility, binary search allows us to directly jump to the relevant candidate.
We use the Python bisect_right
function, which returns the position where the current event’s start time would fit in the list of previously processed event end times.
🔑 Step 3: Maintain Rolling Maximum Values
To keep track of the maximum value of previously processed events, we maintain two lists:
max_weights
: Stores the maximum values observed so far.- At each step, this list tells us the best value we can achieve by attending only one event up to the current point.
max_weight_ends
: Stores the corresponding end times for the values inmax_weights
.- This allows us to check compatibility efficiently using binary search.
Updating max_weights
and max_weight_ends
:
- After processing each event, we compare its value with the current maximum in
max_weights
. - If the new event has a higher value, we append it to
max_weights
along with its end time tomax_weight_ends
.
Example:
For events:
events = [[1, 3, 5], [4, 6, 10], [7, 9, 15]]
- Start with:
max_weights = [0] # Rolling maximum values max_weight_ends = [-1] # Corresponding end times
- After processing
[1, 3, 5]
:max_weights = [0, 5] max_weight_ends = [-1, 3]
- After processing
[4, 6, 10]
:max_weights = [0, 5, 10] max_weight_ends = [-1, 3, 6]
- After processing
[7, 9, 15]
:max_weights = [0, 5, 10, 15] max_weight_ends = [-1, 3, 6, 9]
🔑 Step 4: Calculate the Maximum Sum
For each event, calculate the potential maximum value by combining:
- The value of the current event.
- The maximum value of a compatible previous event (found using binary search).
If the current event’s start time is start
and value is value
:
- Use
bisect_right(max_weight_ends, start - 1)
to find the latest compatible event. - Add its value from
max_weights
. - Update the global maximum (
max_two
) if this combination is better.
🔑 Example Walkthrough
Let’s process a more detailed example:
Input:
events = [[1, 5, 3], [6, 10, 10], [11, 15, 20], [16, 20, 30]]
Step-by-Step Execution:
- Sort Events by End Time:
sorted_events = [[1, 5, 3], [6, 10, 10], [11, 15, 20], [16, 20, 30]]
- Initialize Trackers:
max_weights = [0] # Initial maximum value max_weight_ends = [-1] # Initial end times max_two = 0 # Final result
-
Process Events:
- Event [1, 5, 3]:
- No compatible previous event (
bisect_right([-1], 0) - 1 = -1
). max_two = max(0, 3 + 0) = 3
.- Update trackers:
max_weights = [0, 3] max_weight_ends = [-1, 5]
- No compatible previous event (
- Event [6, 10, 10]:
- Compatible event:
[1, 5, 3]
(bisect_right([5], 5) - 1 = 0
). max_two = max(3, 10 + 3) = 13
.- Update trackers:
max_weights = [0, 3, 10] max_weight_ends = [-1, 5, 10]
- Compatible event:
- Event [11, 15, 20]:
- Compatible event:
[6, 10, 10]
(bisect_right([10], 10) - 1 = 1
). max_two = max(13, 20 + 10) = 30
.- Update trackers:
max_weights = [0, 3, 10, 20] max_weight_ends = [-1, 5, 10, 15]
- Compatible event:
- Event [16, 20, 30]:
- Compatible event:
[11, 15, 20]
(bisect_right([15], 15) - 1 = 2
). max_two = max(30, 30 + 20) = 50
.- Update trackers:
max_weights = [0, 3, 10, 20, 30] max_weight_ends = [-1, 5, 10, 15, 20]
- Compatible event:
- Event [1, 5, 3]:
- Final Result:
max_two = 50
🐍 Python Code
from bisect import bisect_right
class Solution:
def maxTwoEvents(self, events: List[List[int]]) -> int:
# Step 1: Pre-sort events by their ending times
events.sort(key=lambda x: x[1])
# Step 2: Initialize trackers for maximum values and ends
max_weights = [0] # Rolling max weights
max_weight_ends = [-1] # Rolling end times
max_two = 0 # Final result
# Step 3: Iterate over each event
for start, end, value in events:
# Find the max-weight event ending before the current event's start time
index = bisect_right(max_weight_ends, start - 1) - 1
# Update max_two with the best sum of values
if value + max_weights[index] > max_two:
max_two = value + max_weights[index]
# Update rolling max values
if value > max_weights[-1]:
max_weights.append(value)
max_weight_ends.append(end)
return max_two
🔍 Example Walkthrough (With Higher Numbers)
Input:
events = [[1, 5, 10], [6, 10, 15], [11, 15, 20], [16, 20, 25]]
Sorted Events:
[[1, 5, 10], [6, 10, 15], [11, 15, 20], [16, 20, 25]]
Step-by-Step Execution:
- First Event:
[1,5,10]
- No previous non-overlapping event.
max_two = max(0, 10) = 10
.- Update
max_weights = [0, 10]
,max_weight_ends = [-1, 5]
.
- Second Event:
[6,10,15]
- Previous compatible event:
[1,5,10]
. max_two = max(10, 15 + 10) = 25
.- Update
max_weights = [0, 10, 15]
,max_weight_ends = [-1, 5, 10]
.
- Previous compatible event:
- Third Event:
[11,15,20]
- Previous compatible event:
[6,10,15]
. max_two = max(25, 20 + 15) = 35
.- Update
max_weights = [0, 10, 15, 20]
,max_weight_ends = [-1, 5, 10, 15]
.
- Previous compatible event:
- Fourth Event:
[16,20,25]
- Previous compatible event:
[11,15,20]
. max_two = max(35, 25 + 20) = 45
.- Update
max_weights = [0, 10, 15, 20, 25]
,max_weight_ends = [-1, 5, 10, 15, 20]
.
- Previous compatible event:
Output:
45
⏱️ Time Complexity
Sorting:
Sorting the events takes O(n log n), where n
is the number of events.
Binary Search:
For each event, finding the best compatible event takes O(log n) using binary search.
Overall:
O(n log n), which is efficient for the given constraints.
🔚 Conclusion
This problem elegantly combines sorting, binary search, and dynamic programming for an efficient solution. 🧠 By leveraging a clear structure and precomputed results, we ensure that we can handle large inputs efficiently.
Now, you’re ready to tackle event optimization problems with confidence! 💪 Keep exploring, keep optimizing, and happy coding! 🚀