Dynamic Programming · 45 of 60
Weighted Job Scheduling
Pick jobs that do not overlap in time so that the money you earn is as large as possible.
- Simple way
- time O(2^n)
room O(n) - Clever way
- time O(n log n)
room O(n)
The problem, simply
You get n jobs. Each job has a start time, an end time and a profit. You can only do one job at a time, but a job may start exactly when the previous one ends. Choose jobs that do not overlap so the total profit is as big as possible.
Unlike plain activity selection, the jobs pay different amounts, so "do as many jobs as possible" is not the goal.
A story to remember it
Picture it
Five jobs on a time line, sorted by end time. Each bar shows the job name and its profit. E pays the most, but it overlaps almost everything.
Examples
Five jobs
- In
start = [1, 2, 3, 4, 6], end = [3, 5, 10, 6, 9], profit = [20, 20, 100, 70, 60]- Out
150
Do the jobs 1 to 3, 4 to 6 and 6 to 9: 20 + 70 + 60. The 100 job overlaps them.
Four jobs
- In
start = [1, 2, 3, 3], end = [3, 4, 5, 6], profit = [50, 10, 40, 70]- Out
120
1 to 3 for 50, then 3 to 6 for 70. Ending at 3 and starting at 3 is fine.
One long job wins
- In
jobs = (1,2,50), (3,5,20), (6,19,100), (2,100,200)- Out
250
The first job plus the long 200 job. Doing three short jobs earns only 170.
All overlap
- In
start = [1, 1, 1], end = [2, 3, 4], profit = [5, 6, 4]- Out
6
Every job starts at 1, so you can do only one. Pick the best one.
Way 1: the simple way
Sort jobs by end time. Let f(i) be the best profit using only the first i jobs.
f(0) = 0.- Skip job
i:f(i - 1). - Take job
i: its profit plusf(p), wherepcounts the jobs that end by the time jobistarts. Findpby scanning back. f(i)is the bigger of the two.
The calls for the five jobs, three levels deep. n means skip the job, y means take it. f3 and f1 are worked out again and again.
- checking
class Solution {
int[][] jobs; // {start, end, profit}
public int maxProfit(int[] start, int[] end, int[] profit) {
int n = start.length;
jobs = new int[n][];
for (int i = 0; i < n; i++) jobs[i] = new int[] {start[i], end[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // by end time
return f(n);
}
// best profit using the first i jobs
private int f(int i) {
if (i == 0) return 0;
int skip = f(i - 1);
int p = i - 1; // jobs that end by the time job i starts
while (p > 0 && jobs[p - 1][1] > jobs[i - 1][0]) p--;
int take = jobs[i - 1][2] + f(p);
return Math.max(skip, take);
}
}How long it takes: each call can make two more calls, so up to O(2^n). Extra room: the call chain, O(n).
Way 2: the clever way
Store f(i) in a row best[0..n] and fill it from left to right. To find p fast, use binary search on the end times, which are sorted.
- Sort the jobs by end time.
best[0] = 0.- For job
i: binary search forp, the number of jobs whose end is at most jobi's start. best[i] = max(best[i - 1], profit[i] + best[p]).- Return
best[n].
Five jobs, one decision each
01 / 07
Sort the jobs by when they end: A, B, C, D, E. best[0] = 0, since with no jobs you earn nothing.
- looking now
- checking
- seen
- done
- answer
- just added
- ignored
class Solution {
public int maxProfit(int[] start, int[] end, int[] profit) {
int n = start.length;
int[][] jobs = new int[n][];
for (int i = 0; i < n; i++) jobs[i] = new int[] {start[i], end[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // by end time
int[] best = new int[n + 1]; // best[0] = 0
for (int i = 1; i <= n; i++) {
int s = jobs[i - 1][0];
// binary search: how many of the first i-1 jobs end by time s?
int lo = 0, hi = i - 1;
while (lo < hi) {
int mid = (lo + hi + 1) / 2;
if (jobs[mid - 1][1] <= s) lo = mid; else hi = mid - 1;
}
int take = jobs[i - 1][2] + best[lo];
best[i] = Math.max(best[i - 1], take); // skip or take
}
return best[n];
}
}How long it takes: sorting is O(n log n) and each job does one binary search, O(log n), so O(n log n) in all. Extra room: the jobs and the row, O(n).
Watch out for
- Touching ends. A job ending at 3 and one starting at 3 do not overlap here. Use
<=in the search. If your problem says they do overlap, use<. - Greedy picks fail. The most profitable job, the shortest job, or the most jobs can all lose (see the examples).
- Profits can add up past
intfor big inputs. Uselongif needed.
What interviewers ask next
- Print which jobs were chosen. Walk back from
best[n]: ifbest[i] == best[i - 1]the job was skipped, else it was taken and you jump to itsp. - What if all profits are equal? Then it is plain activity selection, and greedy by earliest end time is enough,
O(n log n)with no table. - What if you have
kworkers instead of one? Add a dimension for how many workers are used, or think of it as a min cost flow problem for bigk. - Can you do it without sorting by end? Sort by start and go from right to left:
best[i] = max(best[i + 1], profit[i] + best[next]), wherenextis the first job starting at or after this one ends. SameO(n log n).
Remember this
Grown-up words: the parties are weighted intervals, sorting by end time sets the DP order, the last job that fits is the latest compatible job, finding it fast is binary search, and the equal profit case is the activity selection problem.