LeetCode 1751 - Maximum Number of Events That Can Be Attended II
You are given a list of events, where each event is represented as: - startDay - endDay - value If you attend an event, you must attend the entire interval from startDay to endDay, inclusive.
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Dynamic Programming, Sorting
Solution
Problem Understanding
You are given a list of events, where each event is represented as:
startDayendDayvalue
If you attend an event, you must attend the entire interval from startDay to endDay, inclusive. Because the interval is inclusive, two events conflict if one starts on the same day another ends.
You may attend at most k events, and the goal is to maximize the total collected value.
The challenge comes from combining two constraints at the same time:
- Events cannot overlap.
- You may select at most
kevents.
This immediately suggests that the problem is not just interval scheduling. We are not maximizing the number of events, we are maximizing weighted profit under a limit on how many intervals may be selected.
The input size is large enough that brute force is impossible:
events.lengthcan be up to10^5k * events.length <= 10^6
That second constraint is important. It strongly hints that an O(n * k) dynamic programming solution is expected.
Another important observation is that event days can be as large as 10^9. This means we cannot build a DP based on actual calendar days. We must work directly with the events themselves.
A subtle but extremely important rule is that the end day is inclusive. If one event ends on day 5, another event starting on day 5 is considered overlapping. The next compatible event must start strictly after the current event's end day.
Several edge cases can easily cause bugs:
- Events that touch at endpoints, such as
[1,2]and[2,3] - Situations where taking fewer than
kevents is optimal - Multiple overlapping events with very different values
k = 1, which reduces the problem to selecting the single best event- All events non-overlapping, where the answer becomes selecting the top
kprofitable events in chronological compatibility order
Approaches
Brute Force
The most direct approach is to try every subset of events up to size k.
For each subset, we would:
- Check whether the events overlap.
- Compute the total value.
- Keep the maximum valid total.
This works because it exhaustively explores every possible combination, guaranteeing that the best answer will eventually be found.
Unfortunately, the number of subsets is exponential. With n events, there are 2^n possible subsets. Even if we prune selections larger than k, the complexity is still enormous.
This becomes completely infeasible once n reaches even a few dozen, while the actual constraints allow up to 10^5 events.
Key Insight
This problem is a classic combination of:
- Weighted interval scheduling
- Dynamic programming
- Binary search
The critical observation is that after sorting events by start time, when we decide whether to take an event, we only need to know:
- Which event index we are currently considering
- How many events we may still take
If we choose the current event, we must jump to the next non-overlapping event. Since the events are sorted, we can locate that next compatible event using binary search.
This transforms the exponential search into a dynamic programming problem.
Define:
dp(i, remaining) = maximum value obtainable starting from event i with remaining selections left.
At every event, we have two choices:
- Skip the event
- Take the event and jump to the next compatible event
The recurrence becomes:
- Skip:
dp(i + 1, remaining)
- Take:
value[i] + dp(nextIndex, remaining - 1)
We compute the maximum of these two options.
Because there are only n * k states, and each transition uses binary search, the solution becomes efficient enough for the constraints.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Tries every subset of events |
| Optimal DP + Binary Search | O(n * k * log n) | O(n * k) | Uses memoization and binary search for next compatible event |
Algorithm Walkthrough
- Sort all events by their start day.
Sorting allows us to process events in chronological order and enables binary search for the next compatible event. 2. Extract all start days into a separate array.
This array lets us efficiently binary search for the first event whose start day is strictly greater than the current event's end day. 3. Define a memoized recursive function:
dfs(index, remaining)
This function returns the maximum value obtainable starting from index when we may still attend remaining events.
4. Handle base cases.
If:
index == n, there are no more eventsremaining == 0, we cannot attend additional events
then the answer is 0.
5. Compute the "skip" option.
Ignore the current event and move to the next one:
dfs(index + 1, remaining)
- Compute the "take" option.
If we attend the current event:
- Add its value
- Find the next non-overlapping event using binary search
- Reduce remaining selections by one
value + dfs(nextIndex, remaining - 1)
- Store and return the maximum of the two choices.
Memoization ensures each state is computed only once. 8. Start the recursion from:
dfs(0, k)
Why it works
At every event, the algorithm considers the only two valid possibilities: either attend the event or skip it. Because events are processed in sorted order, binary search correctly identifies the next compatible event. Memoization guarantees that every subproblem is solved once, and every valid combination of non-overlapping events is considered through the recursive decisions. Therefore, the algorithm always finds the optimal total value.
Python Solution
from typing import List
from functools import lru_cache
from bisect import bisect_right
class Solution:
def maxValue(self, events: List[List[int]], k: int) -> int:
events.sort()
start_days = [event[0] for event in events]
n = len(events)
@lru_cache(None)
def dfs(index: int, remaining: int) -> int:
if index == n or remaining == 0:
return 0
skip = dfs(index + 1, remaining)
start, end, value = events[index]
next_index = bisect_right(start_days, end)
take = value + dfs(next_index, remaining - 1)
return max(skip, take)
return dfs(0, k)
The implementation begins by sorting events according to their start day. This guarantees that all future compatible events appear after the current event.
The start_days array exists purely to support binary search. Since we need the first event whose start day is strictly greater than the current event's end day, bisect_right is the perfect tool.
The recursive function dfs(index, remaining) represents the best answer achievable starting from a particular event index with a limited number of selections remaining.
The memoization cache is essential. Without it, the recursion would repeatedly solve identical subproblems and become exponential.
Inside the recursion, two decisions are evaluated:
- Skip the current event
- Take the current event and jump directly to the next compatible event
The algorithm returns whichever choice produces the larger total value.
Go Solution
package main
import (
"sort"
)
func maxValue(events [][]int, k int) int {
sort.Slice(events, func(i, j int) bool {
return events[i][0] < events[j][0]
})
n := len(events)
startDays := make([]int, n)
for i := 0; i < n; i++ {
startDays[i] = events[i][0]
}
memo := make([][]int, n)
for i := 0; i < n; i++ {
memo[i] = make([]int, k+1)
for j := 0; j <= k; j++ {
memo[i][j] = -1
}
}
var dfs func(int, int) int
dfs = func(index int, remaining int) int {
if index == n || remaining == 0 {
return 0
}
if memo[index][remaining] != -1 {
return memo[index][remaining]
}
skip := dfs(index+1, remaining)
end := events[index][1]
value := events[index][2]
nextIndex := sort.Search(n, func(i int) bool {
return startDays[i] > end
})
take := value + dfs(nextIndex, remaining-1)
if take > skip {
memo[index][remaining] = take
} else {
memo[index][remaining] = skip
}
return memo[index][remaining]
}
return dfs(0, k)
}
The Go implementation follows the same algorithmic structure as the Python version.
Instead of Python's lru_cache, Go uses a manually allocated 2D memoization table initialized with -1.
Binary search is implemented with sort.Search, which finds the first index satisfying the condition startDays[i] > end.
Go slices are naturally dynamic, so no special handling for empty arrays is required. Integer overflow is also not a concern here because the maximum possible answer fits comfortably inside Go's int on LeetCode platforms.
Worked Examples
Example 1
events = [[1,2,4],[3,4,3],[2,3,1]]
k = 2
After sorting:
| Index | Event |
|---|---|
| 0 | [1,2,4] |
| 1 | [2,3,1] |
| 2 | [3,4,3] |
Start days:
[1, 2, 3]
Now evaluate recursively.
State: dfs(0, 2)
Current event:
[1,2,4]
Binary search for first start day > 2:
nextIndex = 2
Choices:
| Choice | Value |
|---|---|
| Skip | dfs(1,2) |
| Take | 4 + dfs(2,1) |
State: dfs(2,1)
Current event:
[3,4,3]
Next compatible index:
3
Choices:
| Choice | Value |
|---|---|
| Skip | 0 |
| Take | 3 + 0 = 3 |
Result:
dfs(2,1) = 3
So:
take = 4 + 3 = 7
The skip branch cannot exceed this value, so the final answer becomes:
7
Example 2
events = [[1,2,4],[3,4,3],[2,3,10]]
k = 2
Sorted events:
| Index | Event |
|---|---|
| 0 | [1,2,4] |
| 1 | [2,3,10] |
| 2 | [3,4,3] |
At event [2,3,10], the next compatible event must start after day 3.
Event [3,4,3] starts exactly on day 3, so it overlaps.
Therefore, taking value 10 prevents selecting any other event.
The algorithm correctly identifies that 10 is better than combinations like 4 + 3 = 7.
Final answer:
10
Example 3
events = [[1,1,1],[2,2,2],[3,3,3],[4,4,4]]
k = 3
All events are non-overlapping.
The optimal choice is the three highest-value events:
2 + 3 + 4 = 9
The DP naturally explores all possibilities and discovers this maximum.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * k * log n) | There are n * k DP states, each performing one binary search |
| Space | O(n * k) | Memoization table stores one result per state |
The sorting step costs O(n log n), but the dynamic programming dominates the runtime. Since each state performs only constant work plus a binary search, the total complexity becomes O(n * k * log n).
The memoization table stores all computed states, producing O(n * k) memory usage.
Test Cases
sol = Solution()
# Example 1
assert sol.maxValue([[1,2,4],[3,4,3],[2,3,1]], 2) == 7
# Example 2
assert sol.maxValue([[1,2,4],[3,4,3],[2,3,10]], 2) == 10
# Example 3
assert sol.maxValue([[1,1,1],[2,2,2],[3,3,3],[4,4,4]], 3) == 9
# Single event
assert sol.maxValue([[1,5,100]], 1) == 100
# k larger than number of events
assert sol.maxValue([[1,1,2],[2,2,3]], 10) == 5
# Overlapping events where best single event wins
assert sol.maxValue([[1,10,5],[2,3,100],[4,5,100]], 1) == 100
# Inclusive overlap check
assert sol.maxValue([[1,2,5],[2,3,6]], 2) == 6
# Non-overlapping chain
assert sol.maxValue([[1,1,5],[2,2,6],[3,3,7]], 2) == 13
# All events overlap
assert sol.maxValue([[1,5,1],[1,5,2],[1,5,3]], 2) == 3
# Larger mixed case
assert sol.maxValue(
[[1,2,4],[3,4,3],[2,3,10],[5,6,8],[7,8,9]],
3
) == 27
Test Summary
| Test | Why |
|---|---|
| Example 1 | Standard overlapping and non-overlapping combination |
| Example 2 | Best answer uses fewer than k events |
| Example 3 | All events compatible |
| Single event | Smallest non-trivial input |
| k larger than events | Ensures algorithm handles oversized k |
| Best single event | Verifies optimal value selection |
| Inclusive overlap | Confirms strict non-overlap condition |
| Non-overlapping chain | Tests selecting top compatible values |
| All events overlap | Ensures only one event may be selected |
| Larger mixed case | Stress test with multiple transitions |
Edge Cases
One important edge case is events that touch at endpoints. For example, [1,2] and [2,3] overlap because the end day is inclusive. Many implementations incorrectly allow these events together. This solution avoids that mistake by using bisect_right(start_days, end), which finds the first event whose start day is strictly greater than the current end day.
Another important case is when taking fewer than k events produces the optimal answer. Some incorrect solutions try to force exactly k selections. The DP here does not require selecting exactly k events. It simply explores all valid possibilities up to k, allowing the algorithm to correctly return answers like 10 in Example 2.
A third tricky case occurs when all events overlap. In such situations, the algorithm must correctly identify the single highest-value event. Since every state considers both skipping and taking the current event, the recursion naturally compares all overlapping alternatives and keeps the best one.
A final subtle edge case involves very large day values, up to 10^9. Any solution based on iterating over days would fail either in runtime or memory usage. This implementation never depends on actual calendar ranges. It works entirely with sorted event indices and binary search, making it efficient regardless of how large the day numbers become.