We have a set of jobs, each with a start time, end time, and profit. We want to select a subset of non-overlapping jobs that maximizes total profit. Two jobs overlap if one starts before the other ends (but a job ending at time X does not overlap with a job starting at time X).
This is a weighted job scheduling problem. What makes it harder than the unweighted version (where you maximize the count of non-overlapping intervals) is that each job has a different profit. A greedy choice of the shortest or earliest-ending jobs can fail, since a single high-profit job might be worth more than several low-profit ones that fit in the same time window.
For each job, there are exactly two choices: take it and earn its profit (after which you can only take jobs that do not overlap with it), or skip it. That binary choice over a sequence with overlapping subproblems is what makes dynamic programming the fit here.
n <= 5 * 10^4 --> An O(n^2) solution is about 2.5 billion operations, too slow. The target is O(n log n).startTime[i], endTime[i] <= 10^9 --> Times are too large to index into a time-keyed DP array. The DP must be over job indices, not time values.profit[i] <= 10^4 --> Profits are positive, so taking a job never lowers the total on its own. A job is only worth skipping when it conflicts with a more profitable combination. With at most 5 10^4 jobs each under 10^4, the total fits in a 32-bit signed int (max around 5 10^8), so int does not overflow.Consider every possible subset of non-overlapping jobs and pick the one with the highest total profit. For each job, decide to take it or skip it. Taking it means moving ahead to the next job that does not overlap. Skipping it means moving to the next job.
Sort the jobs by start time so the recursion can process them left to right. Define a recursive function starting from job 0: either include the current job and move to the next non-conflicting one, or skip it and move to the next job.
solve(i) that returns the maximum profit from jobs i through n-1.i >= n, return 0.solve(i + 1).profit[i] + solve(next) where next is the first job whose start time >= end time of job i. Find next by linearly scanning from i + 1.The recursion recomputes the same solve(i) calls many times, and the linear scan for the next job is itself O(n). The next approach caches each result and replaces the scan with binary search.
Memoization caches the result of solve(i) the first time and returns it on later calls, so each subproblem is computed once. That alone is not enough: finding the next non-overlapping job with a linear scan still costs O(n) per job, leaving the overall solution at O(n^2).
The fix for the scan comes from the sort order. With jobs sorted by start time, the end time of job i is a threshold, and we want the first job whose start time is at or after that threshold. Searching for the leftmost qualifying position in a sorted array is a binary search, which drops the per-job lookup from O(n) to O(log n).
Memoization makes each of the n subproblems solve(i) resolve once, and each does O(log n) binary search work. To compute the best profit from job i onward, the recursion needs the best profit from a later job onward, which is either already cached or computed once and cached on first use.
The binary search is valid because sorting by start time leaves the start times in non-decreasing order, so the first start time >= a given end time is the leftmost qualifying position in a sorted array.
solve(i) with memoization: the maximum profit from jobs i through n-1.i >= n, return 0.solve(i + 1).profit[i] + solve(next).max(skip, take).The memoized approach is already O(n log n). The next approach expresses the same recurrence iteratively, filling a DP table from left to right with no recursion stack.
Approach 2 is already O(n log n). Bottom-up DP expresses the same recurrence iteratively, which removes the recursion stack.
Sort jobs by end time. (Either sort order works; end time is the standard formulation for this problem and makes the lookup a search over end times.) Define dp[i] as the maximum profit considering only the first i jobs after sorting. For each job i, there are two choices:
dp[i-1].profit[i] plus the best profit from all jobs that finish at or before job i starts. That means the largest j with endTime[j] <= startTime[i]. Since jobs are sorted by end time, this is a binary search.So dp[i] = max(dp[i-1], profit[i] + dp[j]) where j is found via binary search.
Processing jobs in order of increasing end time means that when job i is considered, every job that could precede it (one ending no later than job i starts) has already been processed and its optimum stored in dp. The binary search locates the index of the latest such job.
The recurrence max(dp[i-1], profit[i] + dp[j]) covers both cases: job i is excluded, so the answer is the best over the first i-1 jobs; or job i is included, so its profit adds to dp[j], the best achievable using only jobs that end before it. Each prefix optimum dp[j] is the best over all compatible subsets, not just the single job at index j, so taking dp[j] rather than scanning earlier entries is enough.
dp array of size n + 1, where dp[0] = 0 (no jobs, no profit).dp[i] = max(dp[i-1], profit[i] + dp[j]).dp[n].