Skip to content
dsa450for five year olds

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

timelinejobs sorted by end time (name and profit)
01234567891011A 20B 20C 70D 60E 100

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.

  1. f(0) = 0.
  2. Skip job i: f(i - 1).
  3. Take job i: its profit plus f(p), where p counts the jobs that end by the time job i starts. Find p by scanning back.
  4. f(i) is the bigger of the two.
treef(i) = best using the first i jobs
nnnyynyynyf5f4f3f2f1f3f2f1f1f0f0againagainagainagain

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
java
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.

  1. Sort the jobs by end time.
  2. best[0] = 0.
  3. For job i: binary search for p, the number of jobs whose end is at most job i's start.
  4. best[i] = max(best[i - 1], profit[i] + best[p]).
  5. Return best[n].

Five jobs, one decision each

01 / 07

gridbest[i] using the first i jobs
noneABCDE0
timelinejobs sorted by end time (name and profit)
01234567891011A 20B 20C 70D 60E 100

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
java
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 int for big inputs. Use long if needed.

What interviewers ask next

  • Print which jobs were chosen. Walk back from best[n]: if best[i] == best[i - 1] the job was skipped, else it was taken and you jump to its p.
  • 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 k workers instead of one? Add a dimension for how many workers are used, or think of it as a min cost flow problem for big k.
  • 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]), where next is the first job starting at or after this one ends. Same O(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.