Taro Logo

Maximum Profit in Job Scheduling

Hard
Amazon logo
Amazon
3 views
Topics:
ArraysBinary SearchDynamic Programming

We have n jobs, where every job is scheduled to be done from startTime[i] to endTime[i], obtaining a profit of profit[i]. You are given the startTime, endTime and profit arrays. Return the maximum profit you can take such that there are no two jobs in the subset with overlapping time ranges.

If you choose a job that ends at time X you will be able to start another job that starts at time X.

Example 1:

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:

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:

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

Can you come up with an efficient algorithm for this problem?

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What are the constraints on the size of the input arrays (startTime, endTime, profit)?
  2. Can the start times, end times, or profits be zero or negative?
  3. Is it possible for any two jobs to have the same start and end times, and if so, how should that be handled?
  4. If no jobs can be scheduled without overlaps, what value should I return?
  5. Is the startTime and endTime arrays already sorted or do I need to sort them?

Brute Force Solution

Approach

The core idea is to explore every single possible combination of jobs we can take. We will check each combination to see if the jobs are compatible and then keep track of the best profit we've found so far.

Here's how the algorithm would work step-by-step:

  1. Consider each job. You have two choices: either take the job or leave it.
  2. If you take a job, check if it overlaps with any of the jobs you've already decided to take. If there's an overlap, this combination of jobs won't work, so discard it.
  3. If the job doesn't overlap, add its profit to your running total of profit for that combination.
  4. If you leave a job, move on to the next job without adding its profit.
  5. Continue doing this for all the jobs, exploring all possible combinations of taking and leaving jobs.
  6. Keep track of the highest profit you've seen so far from any combination of jobs that don't overlap.
  7. After you've considered every combination, the highest profit you've seen is the answer.

Code Implementation

def max_profit_job_scheduling_brute_force(start_times, end_times, profits):
    number_of_jobs = len(start_times)

    def is_overlapping(job_index, scheduled_jobs):
        for scheduled_job_index in scheduled_jobs:
            if max(start_times[job_index], start_times[scheduled_job_index]) < min(end_times[job_index], end_times[scheduled_job_index]):
                return True
        return False

    def find_max_profit(current_job_index, scheduled_jobs):
        # Base case: If we've considered all jobs
        if current_job_index == number_of_jobs:
            total_profit = 0
            for job_index in scheduled_jobs:
                total_profit += profits[job_index]
            return total_profit

        # Option 1: Exclude the current job
        max_profit_excluding_job = find_max_profit(current_job_index + 1, scheduled_jobs)

        # Option 2: Include the current job if it doesn't overlap
        max_profit_including_job = 0

        # Check for overlaps with previously scheduled jobs
        if not is_overlapping(current_job_index, scheduled_jobs):
            # Add the job and recurse
            max_profit_including_job = find_max_profit(current_job_index + 1, scheduled_jobs + [current_job_index])

        # Return the maximum of the two options
        return max(max_profit_excluding_job, max_profit_including_job)

    # Start the recursion with no jobs scheduled.
    return find_max_profit(0, [])

Big(O) Analysis

Time Complexity
O(2^n)The given approach explores every possible subset of the n jobs. For each job, we have two choices: either include it in our schedule or exclude it. This branching decision process leads to 2 * 2 * ... * 2 (n times) possible combinations, which is 2^n. For each of these combinations, we need to check for overlaps, which takes O(n) time in the worst case. However, since the core driver of the complexity is the exploration of 2^n subsets, the overall time complexity is dominated by O(2^n).
Space Complexity
O(1)The plain English explanation describes exploring combinations by choosing to take or leave jobs. This approach doesn't explicitly mention any auxiliary data structures like lists or hash maps to store intermediate results. It describes tracking only a 'running total of profit' and the 'highest profit seen so far', which can be stored in constant space variables, regardless of the number of jobs, N. Therefore, the space complexity is constant.

Optimal Solution

Approach

The goal is to maximize your earnings by strategically picking the best set of jobs. The most efficient approach involves making ordered choices, skipping jobs that conflict with earlier, more profitable ones, and remembering the best profit we have seen so far.

Here's how the algorithm would work step-by-step:

  1. First, sort all the jobs based on when they finish. This helps us consider jobs in a natural order.
  2. Now, go through the jobs one by one in that sorted order.
  3. For each job, consider two options: either take the job or skip it.
  4. If we take the job, figure out what the latest non-overlapping job is that comes before it. This means finding the last job that finishes before the current job starts.
  5. Calculate the profit of taking the current job. That's the current job's profit plus the maximum profit from the latest non-overlapping job.
  6. If we skip the current job, the profit is just whatever the maximum profit was from the previous job we considered.
  7. Choose the option that gives us the highest profit – either take the job and include its profit, or skip the job and keep the previous highest profit.
  8. Keep track of the highest profit seen so far. This will be our final answer after considering all the jobs.

Code Implementation

def job_scheduling(start_time, end_time, profit):
    jobs = sorted(zip(start_time, end_time, profit), key=lambda x: x[1])
    number_of_jobs = len(jobs)
    maximum_profit_at_time = [0] * number_of_jobs

    maximum_profit_at_time[0] = jobs[0][2]

    for i in range(1, number_of_jobs):
        # Calculate profit including the current job
        current_profit = jobs[i][2]
        latest_non_overlapping_job = -1

        # Find the latest non-overlapping job
        for j in range(i - 1, -1, -1):
            if jobs[j][1] <= jobs[i][0]:
                latest_non_overlapping_job = j
                break

        if latest_non_overlapping_job != -1:
            current_profit += maximum_profit_at_time[latest_non_overlapping_job]

        # Choose max profit either including or excluding job
        maximum_profit_at_time[i] = max(current_profit, maximum_profit_at_time[i - 1])

    # The last value holds the max profit
    return maximum_profit_at_time[number_of_jobs - 1]

Big(O) Analysis

Time Complexity
O(n log n)The algorithm begins by sorting the jobs based on their finish times, which takes O(n log n) time. Then, for each of the n jobs, a binary search is performed to find the latest non-overlapping job. Each binary search takes O(log n) time. Since the binary search is inside the loop that iterates through the jobs, the total time complexity is dominated by the sorting step plus the n iterations of the binary search which equals O(n log n) + n * O(log n). Therefore the overall time complexity is O(n log n).
Space Complexity
O(N)The algorithm sorts the jobs, which may require O(N) auxiliary space depending on the sorting algorithm used (e.g., merge sort). Additionally, the algorithm implicitly builds an array to store the maximum profit seen so far for each job considered. The size of this profit array will be proportional to the number of jobs (N), where N is the number of jobs. Therefore, the auxiliary space complexity is O(N).

Edge Cases

CaseHow to Handle
Empty startTime, endTime, or profit arraysReturn 0 immediately as no jobs can be scheduled with empty input.
startTime, endTime, and profit arrays have different lengthsThrow an IllegalArgumentException or return an error code, as the input is invalid.
Single job availableReturn the profit of the single job if valid, as it's the maximum possible profit.
Jobs with overlapping times but no profitThe solution should correctly determine that these jobs should not be scheduled, resulting in 0 profit from those jobs.
Jobs with identical start and end times but different profitsSorting by end time or a custom criteria must handle the case of identical end times, picking the job which helps maximize profit.
Large number of jobs (scalability)Dynamic programming with memoization or a similar efficient approach is needed to handle large input sizes without exceeding time limits.
startTime and endTime values are large, leading to potential integer overflow during calculationsUse long data type for calculations or consider modulo operations if appropriate to prevent integer overflow.
All jobs are mutually exclusiveThe algorithm must correctly identify and sum the profits of all mutually exclusive jobs to determine the maximum profit.