3476. Maximize Profit from Task Assignment 🔒
Problem Description
You are given an integer array workers
, where workers[i]
represents the skill level of the i
th 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:
-
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 useSortedList
for maintaining the profits in descending order. -
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.
-
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.
- If tasks are available, remove the task with the highest profit (using
-
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.
-
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 EvaluatorExample 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]]
-
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]
, add100
tod[3]
. - For task
[3, 150]
, add150
tod[3]
, resulting ind[3] = [150, 100]
. - For task
[4, 200]
, add200
tod[4]
, resulting ind[4] = [200]
. - For task
[5, 300]
, add300
tod[5]
, resulting ind[5] = [300]
.
- For task
- Initialize a dictionary
-
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 removing150
fromd[3]
.- Add
150
toans
, makingans = 150
.
- Worker with skill
4
:d[4]
is not empty, take the highest profit by removing200
fromd[4]
.- Add
200
toans
, makingans = 350
.
- Worker with skill
- Initialize
-
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 toans
, making the totalans = 650
.
- Iterate over remaining tasks in
-
Final Calculation:
- The total maximum profit earned by assigning tasks is
650
.
- The total maximum profit earned by assigning tasks is
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.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
https algomonster s3 us east 2 amazonaws com cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue
Want a Structured Path to Master System Design Too? Don’t Miss This!