Facebook Pixel

3476. Maximize Profit from Task Assignment 🔒


Problem Description

You are given an integer array workers, where workers[i] represents the skill level of the ith worker. You are also given a 2D integer array tasks, where each element consists of two integers: tasks[i][0] represents the skill requirement needed to complete a task, and tasks[i][1] represents the profit earned from completing that task.

Each worker can complete at most one task, and they can only take a task if their skill level matches the task's skill requirement. An additional worker joins, who can take any task, regardless of the skill requirement.

The goal is to determine the maximum total profit that can be earned by optimally assigning the tasks to the workers.

Intuition

To maximize the profit, we must optimally assign tasks to workers based on their skills. A natural way to tackle this is to categorize available tasks by their skill requirements using a hash table. This allows quick access to tasks a particular worker can complete. For each worker, access the list of tasks they qualify for (if any exist) and assign them the task with the highest profit. This is done using a priority queue where tasks are sorted by profit in descending order.

Once all workers have been processed, there might still be tasks left unassigned. Here, the special worker, who can take any remaining task, should be used to take the task with the highest profit among the remaining options. This ensures that the maximum total profit is obtained from available tasks.

The solution is efficient as it focuses on matching workers to tasks with careful consideration of the profit, using data structures that enable both fast insertion and retrieval of maximum values.

Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.

Solution Approach

To solve this problem optimally, we utilize a hash table with priority queues, allowing us to efficiently organize and access tasks based on skill requirements:

  1. Hash Table for Task Grouping: We create a dictionary d where each key is a skill requirement and the associated value is a priority queue. This allows us to quickly find all tasks for a given skill level. We use SortedList for maintaining the profits in descending order.

  2. Populate the Hash Table: For each available task, determined by its skill requirement and profit, append the task's profit to the corresponding skill key in the dictionary. If multiple tasks have the same skill requirement, they will be sorted in descending order of profit.

  3. Assign Tasks to Workers: Iterate through each worker's skill level. For every skill, check if there are available tasks:

    • If tasks are available, remove the task with the highest profit (using pop()) and add the profit to the total accumulated profit.
    • This ensures that the worker takes the highest available profit task they can handle.
  4. Utilize the Additional Worker: After assigning tasks to all regular workers, review the remaining tasks. Use the additional worker to complete the task with the highest profit from any remaining tasks, adding this profit to the total.

  5. Final Calculations: Combine all gathered profits for the solution, ensuring you've maximized the output.

The implementation using these strategies efficiently optimizes the task assignment process, maximizing overall profit. Here’s how the code is structured:

class Solution:
    def maxProfit(self, workers: List[int], tasks: List[List[int]]) -> int:
        d = defaultdict(SortedList)
        for skill, profit in tasks:
            d[skill].add(profit)
      
        ans = 0
        for skill in workers:
            if not d[skill]:
                continue
            ans += d[skill].pop()
      
        mx = 0
        for ls in d.values():
            if ls:
                mx = max(mx, ls[-1])
      
        ans += mx
        return ans

This approach effectively makes use of data structures that ensure fast retrieval and updates, making it well-suited for this problem of maximizing task assignment profits.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through an example to understand the solution approach.

Suppose we have the following input:

  • workers = [3, 4]
  • tasks = [[3, 100], [3, 150], [4, 200], [5, 300]]
  1. Hash Table for Task Grouping:

    • Initialize a dictionary d with keys representing skill levels and values as sorted lists of profits.
    • Populate d:
      • For task [3, 100], add 100 to d[3].
      • For task [3, 150], add 150 to d[3], resulting in d[3] = [150, 100].
      • For task [4, 200], add 200 to d[4], resulting in d[4] = [200].
      • For task [5, 300], add 300 to d[5], resulting in d[5] = [300].
  2. Assign Tasks to Workers:

    • Initialize ans = 0 to accumulate profits.
    • Iterate over the workers array:
      • Worker with skill 3:
        • d[3] is not empty, take the highest profit by removing 150 from d[3].
        • Add 150 to ans, making ans = 150.
      • Worker with skill 4:
        • d[4] is not empty, take the highest profit by removing 200 from d[4].
        • Add 200 to ans, making ans = 350.
  3. Utilize the Additional Worker:

    • Iterate over remaining tasks in d to find the maximum profit task:
      • d[3] has [100].
      • d[5] has [300].
    • The maximum profit is 300.
    • Add 300 from that task to ans, making the total ans = 650.
  4. Final Calculation:

    • The total maximum profit earned by assigning tasks is 650.

Through this example, we've shown how to optimally assign tasks to workers and utilize an additional worker to maximize profits. The algorithm efficiently handles task assignments using a combination of hash tables and sorted lists.

Solution Implementation

1from typing import List
2from collections import defaultdict
3from sortedcontainers import SortedList
4
5class Solution:
6    def maxProfit(self, workers: List[int], tasks: List[List[int]]) -> int:
7        # Initialize a dictionary where each key (skill level) maps to a sorted list of profits
8        skill_to_profits = defaultdict(SortedList)
9
10        # Populate the mapping from skill levels to profits
11        for skill, profit in tasks:
12            skill_to_profits[skill].add(profit)
13
14        total_profit = 0  # This will hold the cumulative profit
15
16        # Assign the best available task (highest profit) for each worker
17        for skill in workers:
18            if not skill_to_profits[skill]:
19                continue  # If no task is available for this skill, skip to the next worker
20            total_profit += skill_to_profits[skill].pop()  # Add the highest profit task for this skill
21
22        max_remaining_profit = 0  # Track the highest profit from unassigned tasks
23
24        # Find the maximum profit available from the unassigned tasks
25        for profit_list in skill_to_profits.values():
26            if profit_list:  # If there are remaining tasks
27                max_remaining_profit = max(max_remaining_profit, profit_list[-1])  # Take the highest profit available
28
29        total_profit += max_remaining_profit  # Add the highest unassigned profit, if any
30
31        return total_profit
32
1import java.util.HashMap;
2import java.util.Map;
3import java.util.PriorityQueue;
4
5class Solution {
6    public long maxProfit(int[] workers, int[][] tasks) {
7        // Create a map to associate each skill level with a priority queue of profits.
8        Map<Integer, PriorityQueue<Integer>> skillToProfits = new HashMap<>();
9
10        // Populate the map with skill as key and profits in descending order using a priority queue.
11        for (var task : tasks) {
12            int skill = task[0];
13            int profit = task[1];
14            skillToProfits
15                .computeIfAbsent(skill, k -> new PriorityQueue<>((a, b) -> b - a))
16                .offer(profit);
17        }
18
19        long totalProfit = 0;
20
21        // Traverse through each worker's skill level.
22        for (int skill : workers) {
23            // If there's a corresponding skill level in the map, add the highest profit task to the total.
24            if (skillToProfits.containsKey(skill)) {
25                var profitsQueue = skillToProfits.get(skill);
26                totalProfit += profitsQueue.poll(); // Add the highest profit available.
27
28                // Remove the skill from map if there are no more profits available for that skill.
29                if (profitsQueue.isEmpty()) {
30                    skillToProfits.remove(skill);
31                }
32            }
33        }
34
35        // Find the highest profit remaining in any skill level's priority queue.
36        int maxRemainingProfit = 0;
37        for (var profitsQueue : skillToProfits.values()) {
38            maxRemainingProfit = Math.max(maxRemainingProfit, profitsQueue.peek());
39        }
40
41        // Add the highest remaining profit to the total profit.
42        totalProfit += maxRemainingProfit;
43      
44        return totalProfit;
45    }
46}
47
1#include <vector>
2#include <unordered_map>
3#include <queue>
4#include <algorithm>
5
6class Solution {
7public:
8    long long maxProfit(std::vector<int>& workers, std::vector<std::vector<int>>& tasks) {
9        // Create a map to associate skill level with a priority queue of task profits
10        std::unordered_map<int, std::priority_queue<int>> skillToProfit;
11
12        // Populate the map with tasks, where each skill level has a priority queue for profits
13        for (const auto& task : tasks) {
14            int skillRequired = task[0];
15            int taskProfit = task[1];
16            skillToProfit[skillRequired].push(taskProfit);
17        }
18
19        long long totalProfit = 0;
20
21        // Iterate over each worker's skill level to find matching tasks
22        for (int workerSkill : workers) {
23            // Check if there are tasks matching the worker's skill level
24            if (skillToProfit.contains(workerSkill)) {
25                auto& profitQueue = skillToProfit[workerSkill];
26                // Add the highest available profit for the current skill level to the total profit
27                totalProfit += profitQueue.top();
28                profitQueue.pop();
29                // If there are no more tasks for this skill, remove the entry from the map
30                if (profitQueue.empty()) {
31                    skillToProfit.erase(workerSkill);
32                }
33            }
34        }
35
36        int maxOtherProfit = 0;
37        // Find the maximum available profit from remaining tasks
38        for (const auto& [skill, profitQueue] : skillToProfit) {
39            maxOtherProfit = std::max(maxOtherProfit, profitQueue.top());
40        }
41      
42        // Add the maximum other profit to the total profit
43        totalProfit += maxOtherProfit;
44
45        return totalProfit;
46    }
47};
48
1// This function calculates the maximum profit based on workers' skills and available tasks.
2// Each task has a required skill level and a profit associated with completing it.
3
4function maxProfit(workers: number[], tasks: number[][]): number {
5    // Create a dictionary to map each skill to a priority queue of task profits.
6    const skillToProfits = new Map<number, MaxPriorityQueue<number>>();
7
8    // Iterate over each task to populate the skillToProfits map.
9    for (const [skill, profit] of tasks) {
10        // If the skill is not already in the map, initialize it with an empty MaxPriorityQueue.
11        if (!skillToProfits.has(skill)) {
12            skillToProfits.set(skill, new MaxPriorityQueue());
13        }
14        // Add the profit to the corresponding skill's priority queue.
15        skillToProfits.get(skill)!.enqueue(profit);
16    }
17
18    let totalProfit = 0;
19
20    // Iterate over each worker's skill level.
21    for (const skill of workers) {
22        const profitQueue = skillToProfits.get(skill);
23        if (profitQueue) {
24            // Add the highest profit task available for the worker's skill.
25            totalProfit += profitQueue.dequeue();
26          
27            // If no more tasks are left for this skill, remove it from the map.
28            if (profitQueue.size() === 0) {
29                skillToProfits.delete(skill);
30            }
31        }
32    }
33
34    let maxProfit = 0;
35
36    // Find the maximum profit among the remaining tasks for any skill level.
37    for (const profitQueue of skillToProfits.values()) {
38        maxProfit = Math.max(maxProfit, profitQueue.front());
39    }
40  
41    // Add the maximum remaining profit to the total profit.
42    totalProfit += maxProfit;
43
44    return totalProfit;
45}
46

Time and Space Complexity

The time complexity of the code is O((n + m) \times \log m), where n is the number of workers and m is the number of tasks. This complexity arises from the operations on the SortedList, which has logarithmic time complexity for insertion and deletion. Since each worker can attempt to take the highest profit task available to them and there is an additional pass through the list of tasks to find the maximum remaining profit, the operations performed on the SortedList make the overall complexity O((n + m) \times \log m).

The space complexity of the code is O(m). This is due to the creation of a defaultdict of SortedList instances, each potentially holding up to m profits (since each task's profit gets added to the corresponding skill level list).

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!


Load More