Most profit from jobs that do not overlap
Each job is a triple [start, end, profit] with start < end and
profit > 0. You can do at most one job at a time, and a job that ends at time t can be
followed by one that starts at t. Return the largest total profit you can earn.
Examples
| Input | Output | Why |
|---|---|---|
jobs = [[1, 3, 50], [2, 4, 10], [3, 5, 40], [3, 6, 70]] | 120 | Take [1, 3, 50] and then [3, 6, 70]. |
jobs = [[1, 3, 20], [2, 5, 20], [3, 10, 100], [4, 6, 70], [6, 9, 60]] | 150 | [1, 3, 20] then [3, 10, 100]: 120. Better: [1, 3, 20], [4, 6, 70], [6, 9, 60] is 150. |
jobs = [[1, 2, 5], [1, 3, 6], [1, 4, 4]] | 6 | All three overlap; take the most valuable. |
Constraints
- 1 ≤ n ≤ 5·10⁴
- 0 ≤ start < end ≤ 10⁹
- 1 ≤ profit ≤ 10⁴
Hints
Hint 1
The previous problem's rule, keep the job that ends earliest, ignores profit. Find an example where it loses money.
Hint 2
Sort the jobs by end time. For the i-th job there are two choices: skip it, or take it together with the best schedule of jobs that end by its start.
Hint 3
best[i] = max(best[i − 1], profit[i] + best[k]), where k counts the jobs that end at or before job i starts. Find k by binary search on the sorted end times.
Where people lose marks
- Greedy by earliest end or by highest profit: both have counterexamples, including the second example above.
- Searching for end times strictly less than the start: a job ending at t is compatible with one starting at t.
- Indexing best[] by job rather than by prefix length, which makes "no compatible job" awkward; best[0] = 0 handles it.
- Recursing over every subset: exponential, and the subproblems repeat.
Dry run on [[1,3,20],[2,5,20],[3,10,100],[4,6,70],[6,9,60]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 6jobs, sorted by end (start–end: profit)1–3: 2002–5: 2014–6: 7026–9: 6033–10: 1004best00
Sort by end time: 1–3: 20, 2–5: 20, 4–6: 70, 6–9: 60, 3–10: 100. best[0] = 0: with no jobs, no profit.
-
Step 2 / 6jobs, sorted by end (start–end: profit)1–3: 2002–5: 2014–6: 7026–9: 6033–10: 1004best00201take20skip0
Job 1–3 (profit 20). 0 earlier jobs end by 1, worth at most best[0] = 0. Take it: 20 + 0 = 20. Skip it: 0. best[1] = 20.
-
Step 3 / 6jobs, sorted by end (start–end: profit)1–3: 2002–5: 2014–6: 7026–9: 6033–10: 1004best00201202take20skip20
Job 2–5 (profit 20). 0 earlier jobs end by 2, worth at most best[0] = 0. Take it: 20 + 0 = 20. Skip it: 20. best[2] = 20.
-
Step 4 / 6jobs, sorted by end (start–end: profit)1–3: 2002–5: 2014–6: 7026–9: 6033–10: 1004best00201202903take90skip20
Job 4–6 (profit 70). 1 earlier job ends by 4, worth at most best[1] = 20. Take it: 70 + 20 = 90. Skip it: 20. best[3] = 90.
-
Step 5 / 6jobs, sorted by end (start–end: profit)1–3: 2002–5: 2014–6: 7026–9: 6033–10: 1004best002012029031504take150skip90
Job 6–9 (profit 60). 3 earlier jobs end by 6, worth at most best[3] = 90. Take it: 60 + 90 = 150. Skip it: 90. best[4] = 150.
-
Step 6 / 6jobs, sorted by end (start–end: profit)1–3: 2002–5: 2014–6: 7026–9: 6033–10: 1004best0020120290315041505take120skip150
Job 3–10 (profit 100). 1 earlier job ends by 3, worth at most best[1] = 20. Take it: 100 + 20 = 120. Skip it: 150. best[5] = 150. The most profit is 150.
How to think about it
With profits attached, no single rule picks the right job up front: a short job that ends early might block one worth ten times as much. So consider every job, in order of end time, and keep the best total achievable using only the jobs seen so far.
Sort by end. Let best[i] be the most profit using the first i jobs. For job
i:
- Skip it:
best[i − 1]. - Take it: its profit plus the best schedule among jobs that end by its start. Because the jobs are
sorted by end, those are exactly the first
kjobs, wherekis found by binary search: the number of end times ≤ this job's start.
That is O(n log n) in total: the sort, plus one binary search per job.
This is the meeting point of two patterns: intervals decide the order, dynamic programming makes the choice, and binary search finds where the compatible jobs stop.
Complexity. Time O(n log n) · Space O(n)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
from bisect import bisect_right
def max_profit_jobs(jobs):
jobs = sorted(jobs, key=lambda j: j[1]) # by end time
ends = [e for _, e, _ in jobs]
best = [0] # best[i]: most profit from the first i jobs
for i, (s, e, p) in enumerate(jobs):
k = bisect_right(ends, s, 0, i) # jobs that end by s
best.append(max(best[i], p + best[k])) # skip it, or take it
return best[-1]#include <vector>
#include <algorithm>
long long solve(std::vector<std::vector<int>> jobs) {
std::sort(jobs.begin(), jobs.end(), [](const std::vector<int>& a, const std::vector<int>& b) { return a[1] < b[1]; });
std::vector<int> ends;
for (const auto& j : jobs) ends.push_back(j[1]);
std::vector<long long> best(jobs.size() + 1, 0); // best[i]: most profit from the first i jobs
for (size_t i = 0; i < jobs.size(); ++i) {
// jobs among the first i that end by this job's start
size_t k = std::upper_bound(ends.begin(), ends.begin() + i, jobs[i][0]) - ends.begin();
best[i + 1] = std::max(best[i], jobs[i][2] + best[k]); // skip it, or take it
}
return best.back();
}class Solution {
public long maxProfitJobs(int[][] jobs) {
int[][] J = jobs.clone();
java.util.Arrays.sort(J, (a, b) -> Integer.compare(a[1], b[1])); // by end time
int n = J.length;
long[] best = new long[n + 1]; // best[i]: most profit from the first i jobs
for (int i = 0; i < n; i++) {
int lo = 0, hi = i; // count earlier jobs with end <= start
while (lo < hi) {
int m = (lo + hi) >>> 1;
if (J[m][1] <= J[i][0]) lo = m + 1; else hi = m;
}
best[i + 1] = Math.max(best[i], J[i][2] + best[lo]); // skip it, or take it
}
return best[n];
}
}function maxProfitJobs(jobs) {
const J = jobs.map(j => j.slice()).sort((a, b) => a[1] - b[1]); // by end time
const ends = J.map(j => j[1]);
const best = [0]; // best[i]: most profit from the first i jobs
J.forEach(([s, , p], i) => {
let lo = 0, hi = i; // count earlier jobs with end <= s
while (lo < hi) {
const m = (lo + hi) >> 1;
if (ends[m] <= s) lo = m + 1; else hi = m;
}
best.push(Math.max(best[i], p + best[lo])); // skip it, or take it
});
return best[J.length];
}fn solve(jobs: &[[i64; 3]]) -> i64 {
let mut j = jobs.to_vec();
j.sort_by_key(|x| x[1]); // by end time
let ends: Vec<i64> = j.iter().map(|x| x[1]).collect();
let mut best = vec![0i64; j.len() + 1]; // best[i]: most profit from the first i jobs
for i in 0..j.len() {
let k = ends[..i].partition_point(|&e| e <= j[i][0]); // earlier jobs that end by the start
best[i + 1] = best[i].max(j[i][2] + best[k]); // skip it, or take it
}
best[j.len()]
}import Data.List (sortOn)
import qualified Data.Map.Strict as M
-- Sort by end time; best maps each end time to the most profit achievable by then.
-- For each job, look up the best total among jobs ending by its start (lookupLE).
solve :: [[Int]] -> Int
solve jobs = snd (M.findMax final)
where
final = foldl step (M.singleton minBound 0) (sortOn (!! 1) jobs)
step best [s, e, p] =
let upto = maybe 0 snd (M.lookupLE s best) -- best schedule finished by s
sofar = snd (M.findMax best) -- best schedule so far (skip this job)
in M.insert e (max sofar (p + upto)) best
Variants to try
- Return the chosen jobs: walk back from the end, taking job i whenever best[i] ≠ best[i − 1].
- Jobs can run on two machines at once: the state needs the end times of both machines.
- Jobs arrive online and you must decide immediately: no algorithm is optimal; this becomes a competitive-analysis question.
Next problems
Total time covered by a set of bookings
Overlapping bookings must not be counted twice. Sort by start time and one pass merges them into blocks.
Open the problem → Medium18 minFewest meeting rooms for a day of meetings
The number of rooms needed is the most meetings that are ever running at once. Sort the starts and the ends and walk through them together.
Open the problem → Medium18 minFewest bookings to cancel so none overlap
Keeping the most bookings means removing the fewest. Sort by end time and keep every booking that fits.
Open the problem →Write a solution and run it against the real test table.
Keep going
Total time covered by a set of bookings
Overlapping bookings must not be counted twice. Sort by start time and one pass merges them into blocks.
Open the problem → Medium18 minFewest meeting rooms for a day of meetings
The number of rooms needed is the most meetings that are ever running at once. Sort the starts and the ends and walk through them together.
Open the problem → Medium18 minFewest bookings to cancel so none overlap
Keeping the most bookings means removing the fewest. Sort by end time and keep every booking that fits.
Open the problem → IntervalsThe pattern behind it
Sort by start to merge and count overlaps; sort by end to choose. One sorted pass replaces checking every pair.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.