Maximum Profit in Job Scheduling

Updated on 13 June, 2025
Maximum Profit in Job Scheduling header image

Problem Statement

Given a set of n jobs where each job is defined by its start time startTime[i], end time endTime[i], and the profit profit[i] it generates, the task is to compute the maximum profit achievable under the constraint that no two selected jobs can have overlapping time intervals. However, a job can start at the exact time another one ends. For example, if one job ends at time X and another starts at X, both can be considered without overlap. The input consists of three arrays: startTime, endTime, and profit, each describing the respective attributes of the jobs.

Examples

Example 1

Input:

startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]

Output:

120

Explanation:

The subset chosen is the first and fourth job.
Time range [1-3]+[3-6] , we get profit of 120 = 50 + 70.

Example 2

Input:

startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60]

Output:

150

Explanation:

The subset chosen is the first, fourth and fifth job.
Profit obtained 150 = 20 + 70 + 60.

Example 3

Input:

startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4]

Output:

6

Constraints

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

Approach and Intuition

  1. Understanding the Nature of Problem:

    • This is essentially an optimization problem where we need to select a subset of non-overlapping intervals that provides the maximum sum of profits. This scenario can be likened to the "Weighted Interval Scheduling Problem".
  2. Sorting and Dynamic Programming (DP):

    • Step 1: Sort the jobs based on their end times. This arrangement allows us to sequentially decide whether to include a specific job in the optimal subset considering previous jobs.
    • Step 2: Use dynamic programming to store the maximum profit that can be obtained up to each job. Create a DP array where dp[i] represents the maximum profit attainable with the first i jobs when all are considered in ascending order of their end time.
  3. Binary Search for Non-Overlapping Job:

    • To decide whether to include a job i, we need to incorporate its profit alongside the maximum profit from all non-overlapping jobs before it. For efficiently finding the last non-overlapping job relative to the current job, use binary search. This search is done on the end times of the jobs before the current job's start time.
  4. DP State Transition:

    • Base Case: dp[0] = 0, indicating no profit with zero jobs.
    • Transition: For each job i, calculate whether to take this job or not:
      • If taking, dp[i] = profit[i] + dp[index] where index is the last job that does not overlap with the current job found via binary search.
      • If not taking, dp[i] = dp[i-1], which means the best solution up to job i is the same as upto job i-1.
    • Result is stored in dp[n], where n is the total number of jobs.

By following the above method, each decision leverages previous computed values storing the best results at each step, and efficiently handles each job in relation to those before it to ensure non-overlap and maximum profit.

Solutions

  • C++
  • Java
cpp
class Solution {
public:
    int calculateMaximumProfit(vector<vector<int>>& tasks) {
        int taskCount = tasks.size(), bestProfit = 0;
        priority_queue<vector<int>, vector<vector<int>>, greater<vector<int>>> minpq;

        for (int i = 0; i < taskCount; i++) {
            int start = tasks[i][0], finish = tasks[i][1], currentProfit = tasks[i][2];
            while (!minpq.empty() && start >= minpq.top()[0]) {
                bestProfit = max(bestProfit, minpq.top()[1]);
                minpq.pop();
            }
            minpq.push({finish, currentProfit + bestProfit});
        }

        while (!minpq.empty()) {
            bestProfit = max(bestProfit, minpq.top()[1]);
            minpq.pop();
        }

        return bestProfit;
    }

    int maxProfitScheduler(vector<int>& startTimes, vector<int>& endTimes, vector<int>& gains) {
        vector<vector<int>> arrangedTasks;
        for (size_t idx = 0; idx < gains.size(); idx++) {
            arrangedTasks.push_back({startTimes[idx], endTimes[idx], gains[idx]});
        }
        
        sort(arrangedTasks.begin(), arrangedTasks.end());
        return calculateMaximumProfit(arrangedTasks);
    }
};

Here's a summary of the solution for the problem that focuses on maximizing profit by scheduling jobs:

The provided C++ code defines a class Solution that includes two main functions for solving the job scheduling problem to achieve maximum profit:

  1. calculateMaximumProfit:

    • Accepts a vector of vectors (tasks), with each inner vector representing a job characterized by a start time, end time, and the profit associated with completing the job.
    • Utilizes a priority queue to maintain the current set of tasks being considered, ensuring that the queue operates in a minimum fashion based on job end times.
    • Iterates over each job, updating the best profit that can be obtained by either taking the current job into consideration or skipping it based on previously considered jobs.
  2. maxProfitScheduler:

    • Accepts separate vectors for start times, end times, and profits of various jobs.
    • Constructs a vector of tasks where each task is a vector consisting of start time, end time, and profit.
    • Sorts these tasks based on start times to prepare them for processing.
    • Calls calculateMaximumProfit with the sorted tasks to determine the maximum profit.

Overall, the implementation strategically combines dynamic programming with a greedy approach by leveraging a priority queue. This allows the function to efficiently determine the maximum profit that can be attained considering job overlaps and scheduling constraints. The use of sorting and a priority queue ensures that at each step, the decision-making process about which job to select next is optimized for the best possible outcome in terms of profit.

java
class Solution {
    class JobComparator implements Comparator<ArrayList<Integer>> {
        public int compare(ArrayList<Integer> job1, ArrayList<Integer> job2) {
            return job1.get(0) - job2.get(0);
        }
    }

    private int calculateMaximumProfit(List<List<Integer>> jobsList) {
        int jobsCount = jobsList.size(), greatestProfit = 0;
        PriorityQueue<ArrayList<Integer>> priorityQueue = new PriorityQueue<>(new JobComparator());

        for (int i = 0; i < jobsCount; i++) {
            int start = jobsList.get(i).get(0), finish = jobsList.get(i).get(1), jobProfit = jobsList.get(i).get(2);

            while (!priorityQueue.isEmpty() && start >= priorityQueue.peek().get(0)) {
                greatestProfit = Math.max(greatestProfit, priorityQueue.peek().get(1));
                priorityQueue.poll();
            }

            ArrayList<Integer> newJob = new ArrayList<>();
            newJob.add(finish);
            newJob.add(jobProfit + greatestProfit);
            priorityQueue.add(newJob);
        }

        while (!priorityQueue.isEmpty()) {
            greatestProfit = Math.max(greatestProfit, priorityQueue.peek().get(1));
            priorityQueue.poll();
        }

        return greatestProfit;
    }

    public int jobScheduling(int[] startTime, int[] endTime, int[] jobProfit) {
        List<List<Integer>> jobsOverview = new ArrayList<>();
        int numJobs = jobProfit.length;
        for (int i = 0; i < numJobs; i++) {
            ArrayList<Integer> jobDetails = new ArrayList<>();
            jobDetails.add(startTime[i]);
            jobDetails.add(endTime[i]);
            jobDetails.add(jobProfit[i]);
            jobsOverview.add(jobDetails);
        }

        jobsOverview.sort(Comparator.comparingInt(job -> job.get(0)));
        return calculateMaximumProfit(jobsOverview);
    }
}

This Java solution to the Maximum Profit in Job Scheduling problem uses dynamic programming with a greedy approach. Start by defining custom classes and helper methods to handle the job-related data and computations effectively. Follow this step-by-step approach embedded in the code provided:

  1. Create a JobComparator class that implements Comparator<ArrayList<Integer>>. This class defines a comparison logic based on job start times to prioritize jobs with earlier start times.
  2. Define the calculateMaximumProfit method, taking a list of jobs and calculating the maximum possible profit:
    • Initialize a priority queue to handle jobs dynamically, ensuring that the job with the earliest end time remains at the front.
    • Traverse through each job, cleaning up the priority queue by removing completed jobs before the current job's start time and updating the current greatest profit.
    • For each job, calculate the accumulated profit if this job is taken, then push this job and its cumulative profit to the queue.
    • Iterate through the remaining elements in the queue to confirm the maximum profit obtained.
  3. The jobScheduling method sets up the problem:
    • It uses input arrays startTime, endTime, and jobProfit to create a structured list of jobs where each job is represented as a list containing start time, end time, and profit.
    • Sort the list of jobs based on start times using a lambda-based comparator.
    • Call calculateMaximumProfit with this sorted list to find and return the maximum profit.

The priority queue handles dynamic job processing efficiently, allowing the algorithm to consistently update the best possible profit scenario based on job overlaps and sequential job scheduling. Combining sorting with a clever use of priority data structures optimizes the solution for complex scenarios involving multiple overlapping jobs.

Comments

No comments yet.