Maximum Profit in Job Scheduling

There are n jobs. Job i starts at time startTime[i], finishes at time endTime[i], and earns profit[i] if you take it.

You can work on only one job at a time, so the jobs you choose must not overlap. A job that finishes at time x may be followed by a job that starts at exactly time x.

Return the largest total profit you can earn from a set of non-overlapping jobs. The jobs are given in no particular order.

Example 1

Input: startTime = [1,2,4,6], endTime = [3,5,7,8], profit = [20,20,70,30] Output: 90 Why: Take the job from 1 to 3 (20) and the job from 4 to 7 (70) for 90. The job from 4 to 7 overlaps both the 2-to-5 and the 6-to-8 jobs.

Example 2

Input: startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4] Output: 6 Why: All three jobs start at time 1, so only one can be taken; the best single job pays 6.

Example 3

Input: startTime = [1,3,5], endTime = [3,5,9], profit = [10,10,10] Output: 30 Why: Each job starts exactly when the previous one ends, so all three fit: 10 + 10 + 10 = 30.

Constraints

  • 1 <= n == startTime.length == endTime.length == profit.length <= 5 · 104
  • 1 <= startTime[i] < endTime[i] <= 109
  • 1 <= profit[i] <= 104

Pattern Path · Dynamic Programming · problem 159 of 169

Greedy rules such as "most profitable first" or "earliest finish first" fail here. For each job you really have two options: take it or skip it.
Sort the jobs by end time. If you take a job, which earlier jobs can still be combined with it?
Those compatible jobs form a prefix of the sorted list. Store the best profit for every prefix and find the right prefix with binary search on end times.
Target: O(n log n) time, O(n) extra space.

Start simple

Try every subset of jobs and keep the most profitable one with no overlaps: O(2ⁿ · n) time; a DP that scans backwards for the last compatible job is O(n²).

The key idea

Sort the jobs by end time. Let best[i] be the maximum profit using only the first i jobs in that order. For job i there are two choices:

  • Skip it: the answer stays best[i].
  • Take it: earn its profit, plus the best you could do with jobs that end no later than it starts. Because the jobs are sorted by end time, those jobs form a prefix, the first j jobs, so the value is best[j] + profit.

So best[i+1] = max(best[i], best[j] + profit). Find j by binary search on the sorted end times: it is the number of end times <= start, which is exactly what bisect_right returns.

Why sorting by end works: every job compatible with job i that comes earlier finishes by its start, and these form a contiguous prefix of the sorted list, so one DP value summarises all of them. A greedy choice (highest profit first, or earliest end first) fails because profits differ; the DP weighs both options for every job.

Complexity

  • Time: O(n log n)
  • Space: O(n)

Python

from bisect import bisect_right


class Solution:
    def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
        jobs = sorted(zip(endTime, startTime, profit))
        ends = [end for end, _, _ in jobs]
        # best[i] = max profit using only the first i jobs (sorted by end time)
        best = [0] * (len(jobs) + 1)
        for i, (end, start, gain) in enumerate(jobs):
            # Jobs 0..j-1 all finish by the time this one starts.
            j = bisect_right(ends, start, 0, i)
            best[i + 1] = max(best[i], best[j] + gain)
        return best[-1]

JavaScript

var jobScheduling = function(startTime, endTime, profit) {
  const jobs = startTime.map((start, i) => [start, endTime[i], profit[i]]);
  jobs.sort((a, b) => a[1] - b[1]);

  // Number of jobs among the first `hi` whose end time is <= t.
  const countEndingBy = (t, hi) => {
    let lo = 0;
    while (lo < hi) {
      const mid = (lo + hi) >> 1;
      if (jobs[mid][1] <= t) lo = mid + 1;
      else hi = mid;
    }
    return lo;
  };

  // best[i] = max profit using only the first i jobs (sorted by end time)
  const best = new Array(jobs.length + 1).fill(0);
  for (let i = 0; i < jobs.length; i++) {
    const [start, , gain] = jobs[i];
    const j = countEndingBy(start, i);
    best[i + 1] = Math.max(best[i], best[j] + gain);
  }
  return best[jobs.length];
};

What the interviewer is checking

This is weighted interval scheduling. The interviewer wants to see you reject greedy with a counterexample, set up a take-or-skip DP over sorted jobs, and speed up the transition with binary search.

Mistakes to avoid

  • Treating a job that starts exactly when another ends as overlapping (use <= when comparing end to start).
  • Sorting by start time but looking up compatible jobs by end time, or losing the pairing of start, end and profit when sorting.
  • Using a greedy rule, which picks the wrong jobs when one long job pays more or less than several short ones.
  • Scanning backwards linearly for the previous compatible job, which is O(n²) on large inputs.

Follow-ups to expect

  • Return the chosen jobs, not just the profit. (Backtrack through the DP table.)
  • What if you can run up to k jobs at once? (Harder: min-cost flow formulation.)
  • What if all profits are equal? (Greedy by end time suffices.)

In real software

Ad schedulers, freelance gig planners and cloud spot-instance bidders all choose a non-overlapping set of time slots that maximises total value.

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url