Maximum Score of Non-overlapping Intervals
HardPrompt
You are given a 2D integer array intervals, where intervals[i] = [li, ri, weighti]. Interval i starts at position li and ends at ri, and has a weight of weighti. You can choose up to 4 non-overlapping intervals. The score of the chosen intervals is defined as the total sum of their weights.
Return the lexicographically smallest array of at most 4 indices from intervals with maximum score, representing your choice of non-overlapping intervals.
Two intervals are said to be non-overlapping if they do not share any points. In particular, intervals sharing a left or right boundary are considered overlapping.
Example 1:
Input: intervals = [[1,3,2],[4,5,2],[1,5,5],[6,9,3],[6,7,1],[8,9,1]]
Output: [2,3]
Explanation:
You can choose the intervals with indices 2, and 3 with respective weights of 5, and 3.
Example 2:
Input: intervals = [[5,8,1],[6,7,7],[4,7,3],[9,10,6],[7,8,2],[11,14,3],[3,5,5]]
Output: [1,3,5,6]
Explanation:
You can choose the intervals with indices 1, 3, 5, and 6 with respective weights of 7, 6, 3, and 5.
Constraints:
1 <= intevals.length <= 5 * 104intervals[i].length == 3intervals[i] = [li, ri, weighti]1 <= li <= ri <= 1091 <= weighti <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves checking every possible valid selection of intervals. We can choose 1, 2, 3, or 4 intervals. The method iterates through all combinations of k intervals (for k from 1 to 4), checks if they are non-overlapping, and calculates their total weight. It keeps track of the combination that yields the maximum score, adhering to the lexicographical requirement for tie-breaking.
Algorithm
- Initialize
max_score = 0andbest_indices = []. - Iterate through
kfrom 1 to 4. - For each
k, generate all combinations ofkdistinct indices from0ton-1. - For each combination of indices:
- Fetch the corresponding intervals.
- Check if these
kintervals are mutually non-overlapping. This check takesO(k^2)time. - If they are non-overlapping:
- Calculate the sum of their weights.
- If the current sum is greater than
max_score, updatemax_scorewith the new sum andbest_indiceswith the current combination of indices (sorted). - If the current sum is equal to
max_score, compare the current combination of indices (sorted) withbest_indicesand updatebest_indicesif the current one is lexicographically smaller.
Walkthrough
The brute-force method is the most straightforward way to conceptualize the problem. We need to find the best subset of at most 4 intervals. We can break this down by the size of the subset, k.
For each k in {1, 2, 3, 4}:
- Generate all unique combinations of
kintervals from the input list. - For each combination, verify the non-overlapping constraint. A set of intervals is non-overlapping if for any pair of intervals
[l1, r1]and[l2, r2]in the set, eitherr1 < l2orr2 < l1. - If the non-overlapping constraint is met, compute the sum of weights for the intervals in the current combination.
- Compare this sum with the maximum score found so far. If the current score is higher, we've found a new best set. If the scores are equal, we compare the lists of indices lexicographically and keep the smaller one.
This process ensures we check every possibility, guaranteeing we find the optimal answer. However, the number of combinations, especially for k=4, is enormous (C(n, 4) is on the order of n^4), making this approach computationally infeasible for the given constraints.
Complexity
Time
O(n^4). The dominant part is generating and checking combinations of 4 intervals. The number of combinations of 4 from `n` is `C(n, 4) ≈ n^4 / 24`. For each combination, we perform an `O(k^2)` check for overlaps. Thus, the total complexity is roughly `O(n^4 * k^2)`, which is dominated by `n^4`.
Space
O(k), where k is the number of chosen intervals (at most 4). This space is used to store the current combination of indices.
Trade-offs
Pros
Simple to understand and implement.
Guaranteed to find the correct answer if it runs to completion.
Cons
Extremely inefficient and will not pass within the time limits for the given constraints.
The number of combinations
C(n, k)grows very rapidly withn.
A more efficient solution uses dynamic programming. The key insight is to process intervals in a sorted order and build up solutions for choosing k intervals based on optimal solutions for k-1 intervals. By sorting intervals by their end times, we can efficiently find the latest possible non-overlapping predecessor for any given interval using binary search.
Algorithm
- Augment each interval with its original index:
(start, end, weight, original_index). - Sort the intervals based on their end times. Use start times as a tie-breaker.
- Create a DP table
dp[n+1][5].dp[i][k]will store a pair:{long score, List<Integer> indices}. - Initialize
dp[0][k]with score 0 and an empty list for allk. - Iterate
ifrom 1 ton:- Let
current_intervalbe the(i-1)-th interval in the sorted list. - Iterate
kfrom 1 to 4:- Option 1 (Don't take
current_interval): The result isdp[i-1][k]. - Option 2 (Take
current_interval):- Find the rightmost interval
pin0...i-2that ends beforecurrent_intervalstarts. This can be done with binary search on the end times, takingO(log i)time. - The previous state is
dp[p+1][k-1]. - The new result is
{current_interval.weight + dp[p+1][k-1].score, ...}.
- Find the rightmost interval
- Compare the results from Option 1 and Option 2. Choose the one with the higher score. If scores are tied, choose the one with the lexicographically smaller index list. Store this in
dp[i][k].
- Option 1 (Don't take
- Let
- After filling the table, find the best result among
dp[n][1], dp[n][2], dp[n][3], dp[n][4]by applying the same comparison logic. - Return the list of indices from the final best result.
Walkthrough
This approach systematically builds the optimal solution. Here's a detailed breakdown:
-
Data Preparation: We first encapsulate the interval data (
start,end,weight) along with its original index into a custom object or struct. This is crucial for tracking the indices for the final output. -
Sorting: The intervals are sorted primarily by their end points in ascending order. If end points are equal, we can use start points as a secondary sorting criterion. This ordering is key to the DP transition's efficiency.
-
DP State: We define
dp[i][k]to be the optimal result (containing both the maximum score and the corresponding lexicographically smallest list of indices) for choosing exactlyknon-overlapping intervals from the firstiintervals of our sorted list. -
DP Transition: We fill the
dptable iteratively. For each intervaliand countk, we decide whether to include intervaliin our set:- Case 1: Exclude interval
i. The best we can do is the optimal solution forkintervals using the firsti-1intervals, which isdp[i-1][k]. - Case 2: Include interval
i. To do this, we must have chosenk-1intervals that are all non-overlapping with intervali. Since intervals are sorted by end times, all such predecessors must end before intervalibegins. We need to find the optimal solution fork-1intervals that satisfies this. We can use binary search on the sorted intervals0...i-1to find the indexpof the last interval that ends beforeintervals[i].start. The best result for thek-1predecessors is then given bydp[p+1][k-1]. We addintervals[i].weightto its score andintervals[i].original_indexto its index list.
- Case 1: Exclude interval
-
Decision: We compare the results from Case 1 and Case 2. The one with the higher score wins. In case of a tie in score, the one with the lexicographically smaller index list is chosen. This result is stored in
dp[i][k]. -
Final Result: After the DP table is fully computed, the problem asks for at most 4 intervals. So, we find the overall best result by comparing
dp[n][1],dp[n][2],dp[n][3], anddp[n][4]using the same score/lexicographical comparison logic.
import java.util.*; class Solution { // Helper class to store result pair static class Result { long score; List<Integer> indices; Result(long score, List<Integer> indices) { this.score = score; this.indices = indices; } } // Helper class for intervals static class Interval { int start, end, weight, id; Interval(int start, int end, int weight, int id) { this.start = start; this.end = end; this.weight = weight; this.id = id; } } public int[] maxScore(int[][] intervals) { int n = intervals.length; Interval[] sortedIntervals = new Interval[n]; for (int i = 0; i < n; i++) { sortedIntervals[i] = new Interval(intervals[i][0], intervals[i][1], intervals[i][2], i); } // Sort by end time, then start time Arrays.sort(sortedIntervals, (a, b) -> { if (a.end != b.end) { return Integer.compare(a.end, b.end); } return Integer.compare(a.start, b.start); }); Result[][] dp = new Result[n + 1][5]; for (int k = 0; k <= 4; k++) { dp[0][k] = new Result(0, new ArrayList<>()); } for (int i = 1; i <= n; i++) { dp[i][0] = new Result(0, new ArrayList<>()); Interval current = sortedIntervals[i - 1]; for (int k = 1; k <= 4; k++) { // Option 1: Don't include current interval Result res1 = dp[i - 1][k]; // Option 2: Include current interval // Find the last interval j < i-1 that doesn't overlap with current int p = binarySearch(sortedIntervals, i - 1, current.start); Result prevRes = dp[p + 1][k - 1]; long newScore = current.weight + prevRes.score; List<Integer> newIndices = new ArrayList<>(prevRes.indices); newIndices.add(current.id); Collections.sort(newIndices); // Keep it sorted for lexicographical comparison Result res2 = new Result(newScore, newIndices); // Compare and choose the better result dp[i][k] = better(res1, res2); } } Result finalResult = new Result(0, new ArrayList<>()); for (int k = 1; k <= 4; k++) { finalResult = better(finalResult, dp[n][k]); } int[] resultArr = new int[finalResult.indices.size()]; for (int i = 0; i < finalResult.indices.size(); i++) { resultArr[i] = finalResult.indices.get(i); } return resultArr; } // Returns the index of the rightmost interval ending before start_time private int binarySearch(Interval[] intervals, int high, int startTime) { int low = 0; int ans = -1; while (low <= high) { int mid = low + (high - low) / 2; if (intervals[mid].end < startTime) { ans = mid; low = mid + 1; } else { high = mid - 1; } } return ans; } // Compares two results and returns the better one private Result better(Result r1, Result r2) { if (r1.score > r2.score) { return r1; } if (r2.score > r1.score) { return r2; } // Scores are equal, compare indices lexicographically for (int i = 0; i < Math.min(r1.indices.size(), r2.indices.size()); i++) { if (r1.indices.get(i) < r2.indices.get(i)) { return r1; } if (r2.indices.get(i) < r1.indices.get(i)) { return r2; } } return r1.indices.size() < r2.indices.size() ? r1 : r2; }}Complexity
Time
O(n * k * (log n + k)). Sorting takes `O(n log n)`. The nested loops run `n * k` times. Inside the loop, the binary search takes `O(log n)`, and creating/comparing index lists takes `O(k)`. With `k=4`, this is `O(n * (log n + 4))`, which simplifies to `O(n log n)`.
Space
O(n * k^2). The DP table has `(n+1) x (k+1)` entries. Each entry stores a `Result` object, which contains a list of up to `k` indices. With `k=4`, the space complexity is `O(16n)`, which simplifies to `O(n)`.
Trade-offs
Pros
Highly efficient, with a time complexity that handles the given constraints well.
Systematically finds the optimal solution by building upon smaller subproblems.
Correctly handles the lexicographical tie-breaking rule.
Cons
More complex to implement than a brute-force approach.
Requires careful handling of DP states, which include both a score and a list of indices.
The binary search and logic for choosing predecessors must be implemented correctly.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.