You are given three arrays of equal length: startTime
, endTime
, and profit
, where each element represents a job. Specifically, the i
-th job starts at startTime[i]
, ends at endTime[i]
, and yields a profit of profit[i]
if taken.
You can choose any subset of these jobs to schedule, but you cannot select two jobs that overlap in time (that is, a job cannot start before another job ends). Your goal is to select a subset of non-overlapping jobs to maximize the total profit.
Constraints:
At first glance, this problem looks similar to other scheduling problems, like the classic "activity selection" or "weighted interval scheduling." A brute-force approach would be to try every possible subset of jobs, check if they overlap, and calculate the profit for each valid subset. However, with up to 5,000 jobs, this approach is infeasible, as the number of subsets grows exponentially.
Instead, we can look for patterns:
We'll use a dynamic programming approach optimized with binary search. Here's how:
Why these choices?
Example Input:
startTime = [1, 2, 3, 3]
endTime = [3, 4, 5, 6]
profit = [50, 10, 40, 70]
Step 1: Combine and Sort Jobs
Jobs (start, end, profit) after sorting by end time:
Step 2: DP Iterations
Explanation: The optimal solution is to take jobs 0 and 3, for a total profit of 50 + 70 = 120.
from bisect import bisect_right
class Solution:
def jobScheduling(self, startTime, endTime, profit):
jobs = sorted(zip(startTime, endTime, profit), key=lambda x: x[1])
n = len(jobs)
dp = [0] * (n + 1)
ends = [job[1] for job in jobs]
for i in range(1, n + 1):
s, e, p = jobs[i - 1]
# Find the last job that ends before the current job starts
idx = bisect_right(ends, s, 0, i - 1)
dp[i] = max(dp[i - 1], dp[idx] + p)
return dp[n]
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {
int n = startTime.size();
vector<tuple<int,int,int>> jobs;
for (int i = 0; i < n; ++i)
jobs.emplace_back(startTime[i], endTime[i], profit[i]);
sort(jobs.begin(), jobs.end(), [](auto& a, auto& b) {
return get<1>(a) < get<1>(b);
});
vector<int> dp(n+1, 0);
vector<int> ends;
for (auto& job : jobs) ends.push_back(get<1>(job));
for (int i = 1; i <= n; ++i) {
int s = get<0>(jobs[i-1]), p = get<2>(jobs[i-1]);
auto it = upper_bound(ends.begin(), ends.begin() + i - 1, s);
int idx = distance(ends.begin(), it);
dp[i] = max(dp[i-1], dp[idx] + p);
}
return dp[n];
}
};
import java.util.*;
class Solution {
public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = startTime.length;
int[][] jobs = new int[n][3];
for (int i = 0; i < n; i++) {
jobs[i][0] = startTime[i];
jobs[i][1] = endTime[i];
jobs[i][2] = profit[i];
}
Arrays.sort(jobs, Comparator.comparingInt(a -> a[1]));
int[] dp = new int[n + 1];
int[] ends = new int[n];
for (int i = 0; i < n; i++) ends[i] = jobs[i][1];
for (int i = 1; i <= n; i++) {
int s = jobs[i-1][0], p = jobs[i-1][2];
int idx = Arrays.binarySearch(ends, 0, i - 1, s);
if (idx < 0) idx = -idx - 1;
dp[i] = Math.max(dp[i-1], dp[idx] + p);
}
return dp[n];
}
}
var jobScheduling = function(startTime, endTime, profit) {
let n = startTime.length;
let jobs = [];
for (let i = 0; i < n; i++) {
jobs.push([startTime[i], endTime[i], profit[i]]);
}
jobs.sort((a, b) => a[1] - b[1]);
let dp = new Array(n + 1).fill(0);
let ends = jobs.map(job => job[1]);
for (let i = 1; i <= n; i++) {
let s = jobs[i-1][0], p = jobs[i-1][2];
// Binary search for last non-conflicting job
let left = 0, right = i - 1;
while (left < right) {
let mid = Math.floor((left + right) / 2);
if (ends[mid] <= s) left = mid + 1;
else right = mid;
}
let idx = (ends[left] <= s) ? left + 1 : left;
dp[i] = Math.max(dp[i-1], dp[idx] + p);
}
return dp[n];
};
Brute-force: The brute-force approach would try every subset of jobs, which is O(2^n)
time and space, and is infeasible for large n
.
Optimized DP + Binary Search:
O(n \log n)
O(\log n)
per job, for n
jobs is O(n \log n)
O(n \log n)
O(n)
for the DP array and auxiliary arrays
The "Maximum Profit in Job Scheduling" problem is a classic example of weighted interval scheduling, where we need to select non-overlapping jobs to maximize profit. By sorting jobs by end time, using dynamic programming to build up solutions, and binary search to quickly find compatible jobs, we achieve an efficient O(n \log n)
solution. The key insights are recognizing the structure of the problem and using the right data structures for performance, making the solution both elegant and practical.