3476. Maximize Profit from Task Assignment 🔒
Problem Description
You have a group of workers and a list of tasks. Each worker has a skill level given in the array workers
, where workers[i]
is the skill level of the i-th worker. Each task has two properties given in the 2D array tasks
:
tasks[i][0]
is the skill requirement needed to complete that tasktasks[i][1]
is the profit earned from completing that task
The matching rules are:
- Each worker can complete at most one task
- A worker can only take a task if their skill level exactly matches the task's skill requirement
- There's an additional special worker who joins today who can take any task regardless of skill requirement
Your goal is to assign tasks to workers (including the special worker) to maximize the total profit. Each task can only be assigned to one worker, and each worker can only do one task.
For example, if you have workers with skills [2, 3]
and tasks [[2, 5], [3, 10], [4, 15]]
:
- Worker with skill 2 can only do the task requiring skill 2 (profit 5)
- Worker with skill 3 can only do the task requiring skill 3 (profit 10)
- The special worker can do any task, so they would take the task requiring skill 4 (profit 15)
- Total maximum profit would be 5 + 10 + 15 = 30
The function should return the maximum total profit possible from optimally assigning tasks to all available workers.
Intuition
The key insight is that regular workers have strict constraints - they can only do tasks matching their exact skill level. So we should first assign tasks to regular workers greedily, giving each worker the highest-profit task they're qualified for.
Think of it this way: if a worker with skill level 5 can do multiple tasks requiring skill 5, we want them to take the most profitable one. Since each worker can only do one task, we need to track which tasks are available for each skill level.
We can group tasks by their skill requirements using a hash table. For each skill level, we maintain a list of available profits, sorted in descending order (or use a max heap). When a worker with that skill comes along, we give them the best available task (highest profit) and remove it from the pool.
After all regular workers have been assigned their optimal tasks, we have the special worker who can do ANY remaining task. Since this worker has no restrictions, they should simply take the single most profitable task left across all skill levels.
The approach becomes:
- Group tasks by skill requirement in a hash table
d[skill] = [list of profits]
- For each regular worker, check if there are tasks matching their skill
- If yes, assign them the highest-profit task from that skill group
- After all regular workers are assigned, find the maximum profit among ALL remaining tasks
- Assign that task to the special worker
This greedy strategy works because:
- Regular workers are limited by skill, so we maximize their individual contributions first
- The special worker's flexibility is best used to grab whatever single highest-value task remains
- We're not losing anything by being greedy since each worker-task assignment is independent
Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.
Solution Approach
The implementation uses a hash table combined with sorted lists to efficiently manage and assign tasks.
Data Structure Setup:
We use a defaultdict
with SortedList
as values. The hash table d
maps each skill requirement to a sorted list of profits for tasks with that requirement. The SortedList
maintains profits in ascending order, allowing us to efficiently access the maximum profit (last element) for each skill level.
d = defaultdict(SortedList) for skill, profit in tasks: d[skill].add(profit)
Assigning Tasks to Regular Workers: We iterate through each worker and check if there are any tasks matching their skill level. If tasks exist for that skill, we take the most profitable one (the last element in the sorted list) and remove it from the list.
ans = 0 for skill in workers: if not d[skill]: continue ans += d[skill].pop() # pop() removes and returns the last (maximum) element
The pop()
operation on SortedList
removes the last element, which is the maximum profit task for that skill level. This ensures each regular worker gets their best possible task.
Assigning Task to Special Worker: After regular workers are assigned, we find the single most profitable task remaining across all skill levels. We iterate through all remaining sorted lists and find the maximum profit task.
mx = 0
for ls in d.values():
if ls:
mx = max(mx, ls[-1]) # ls[-1] is the maximum in each sorted list
ans += mx
The special worker takes this maximum profit task, regardless of its skill requirement.
Time Complexity:
- Building the hash table:
O(n log n)
wheren
is the number of tasks (due to sorted list insertions) - Assigning to regular workers:
O(m log n)
wherem
is the number of workers - Finding maximum for special worker:
O(k)
wherek
is the number of distinct skill levels - Overall:
O((n + m) log n)
Space Complexity: O(n)
to store all tasks in the hash table and sorted lists.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Given:
- Workers:
[2, 3, 5]
(3 regular workers) - Tasks:
[[2, 10], [2, 8], [3, 15], [5, 20], [5, 12], [7, 25]]
- Plus 1 special worker who can do any task
Step 1: Group tasks by skill requirement
We create a hash table where each skill level maps to a sorted list of profits:
d = { 2: SortedList([8, 10]) # Two tasks need skill 2 3: SortedList([15]) # One task needs skill 3 5: SortedList([12, 20]) # Two tasks need skill 5 7: SortedList([25]) # One task needs skill 7 }
Step 2: Assign tasks to regular workers
Process each regular worker:
-
Worker with skill 2:
- Check
d[2]
→ has tasks[8, 10]
- Take the maximum (10) using
pop()
- Running total:
ans = 10
d[2]
now contains[8]
- Check
-
Worker with skill 3:
- Check
d[3]
→ has task[15]
- Take the maximum (15) using
pop()
- Running total:
ans = 10 + 15 = 25
d[3]
now empty
- Check
-
Worker with skill 5:
- Check
d[5]
→ has tasks[12, 20]
- Take the maximum (20) using
pop()
- Running total:
ans = 25 + 20 = 45
d[5]
now contains[12]
- Check
Step 3: Find best remaining task for special worker
Remaining tasks in the hash table:
d = { 2: [8] # One task left 3: [] # Empty 5: [12] # One task left 7: [25] # One task left (untouched) }
Find the maximum profit among all remaining tasks:
- Skill 2 has max profit: 8
- Skill 3 has no tasks
- Skill 5 has max profit: 12
- Skill 7 has max profit: 25
The special worker takes the task with skill requirement 7 and profit 25.
Final Result:
Total profit = 45 + 25 = 70
This example demonstrates how:
- Regular workers are constrained to tasks matching their exact skill level
- We greedily assign each regular worker their best available task
- The special worker's flexibility is used to grab the single most valuable remaining task across all skill levels
Solution Implementation
1from collections import defaultdict
2from sortedcontainers import SortedList
3from typing import List
4
5class Solution:
6 def maxProfit(self, workers: List[int], tasks: List[List[int]]) -> int:
7 # Create a dictionary mapping skill levels to sorted lists of profits
8 # Each skill level can have multiple tasks with different profits
9 skill_to_profits = defaultdict(SortedList)
10
11 # Populate the dictionary with tasks grouped by skill level
12 # SortedList maintains profits in ascending order automatically
13 for skill_level, profit in tasks:
14 skill_to_profits[skill_level].add(profit)
15
16 # Track total profit accumulated
17 total_profit = 0
18
19 # Assign each worker to their best matching task
20 for worker_skill in workers:
21 # Check if there are any tasks matching this worker's skill level
22 if not skill_to_profits[worker_skill]:
23 continue
24
25 # Assign the highest profit task for this skill level to the worker
26 # pop() removes and returns the last (highest) element
27 total_profit += skill_to_profits[worker_skill].pop()
28
29 # Find the maximum remaining profit across all skill levels
30 # This represents one additional task that could be completed
31 max_remaining_profit = 0
32 for profit_list in skill_to_profits.values():
33 if profit_list:
34 # The last element in SortedList is the maximum
35 max_remaining_profit = max(max_remaining_profit, profit_list[-1])
36
37 # Add the best remaining task's profit to the total
38 total_profit += max_remaining_profit
39
40 return total_profit
41
1class Solution {
2 public long maxProfit(int[] workers, int[][] tasks) {
3 // Map to store tasks grouped by skill level
4 // Key: skill level, Value: max heap of profits for that skill level
5 Map<Integer, PriorityQueue<Integer>> skillToProfitsMap = new HashMap<>();
6
7 // Group tasks by skill level and store profits in max heap
8 for (int[] task : tasks) {
9 int skillRequired = task[0];
10 int taskProfit = task[1];
11
12 // Create max heap if skill level doesn't exist, then add profit
13 skillToProfitsMap.computeIfAbsent(skillRequired, k -> new PriorityQueue<>((a, b) -> b - a))
14 .offer(taskProfit);
15 }
16
17 // Total profit accumulated
18 long totalProfit = 0;
19
20 // Assign each worker to their best matching task (exact skill match)
21 for (int workerSkill : workers) {
22 if (skillToProfitsMap.containsKey(workerSkill)) {
23 PriorityQueue<Integer> profitQueue = skillToProfitsMap.get(workerSkill);
24
25 // Assign the highest profit task for this skill level
26 totalProfit += profitQueue.poll();
27
28 // Remove skill level entry if no more tasks available
29 if (profitQueue.isEmpty()) {
30 skillToProfitsMap.remove(workerSkill);
31 }
32 }
33 }
34
35 // Find the maximum profit among all remaining unassigned tasks
36 int maxRemainingProfit = 0;
37 for (PriorityQueue<Integer> profitQueue : skillToProfitsMap.values()) {
38 maxRemainingProfit = Math.max(maxRemainingProfit, profitQueue.peek());
39 }
40
41 // Add the highest remaining profit to total
42 totalProfit += maxRemainingProfit;
43
44 return totalProfit;
45 }
46}
47
1class Solution {
2public:
3 long long maxProfit(vector<int>& workers, vector<vector<int>>& tasks) {
4 // Map skill level to a max heap of profits for tasks requiring that skill
5 unordered_map<int, priority_queue<int>> skillToProfitHeap;
6
7 // Group tasks by required skill level, storing profits in max heaps
8 for (const auto& task : tasks) {
9 int requiredSkill = task[0];
10 int taskProfit = task[1];
11 skillToProfitHeap[requiredSkill].push(taskProfit);
12 }
13
14 long long totalProfit = 0;
15
16 // Assign each worker to the most profitable task matching their skill
17 for (int workerSkill : workers) {
18 if (skillToProfitHeap.contains(workerSkill)) {
19 // Get the max heap for this skill level
20 auto& profitHeap = skillToProfitHeap[workerSkill];
21
22 // Add the most profitable task to total
23 totalProfit += profitHeap.top();
24 profitHeap.pop();
25
26 // Remove empty heap entries to keep map clean
27 if (profitHeap.empty()) {
28 skillToProfitHeap.erase(workerSkill);
29 }
30 }
31 }
32
33 // Find the maximum profit among all remaining unassigned tasks
34 int maxRemainingProfit = 0;
35 for (const auto& [skill, profitHeap] : skillToProfitHeap) {
36 maxRemainingProfit = max(maxRemainingProfit, profitHeap.top());
37 }
38
39 // Add the best remaining task profit to the total
40 totalProfit += maxRemainingProfit;
41
42 return totalProfit;
43 }
44};
45
1/**
2 * Calculates maximum profit by assigning workers to tasks based on skill levels
3 * @param workers - Array of worker skill levels
4 * @param tasks - 2D array where each element is [skill requirement, profit]
5 * @returns Maximum total profit achievable
6 */
7function maxProfit(workers: number[], tasks: number[][]): number {
8 // Map to store tasks grouped by skill requirement
9 // Key: skill level, Value: MaxPriorityQueue of profits
10 const skillToProfitQueue = new Map<number, any>();
11
12 // Group tasks by skill requirement and store profits in max priority queues
13 for (const [skillRequirement, profit] of tasks) {
14 if (!skillToProfitQueue.has(skillRequirement)) {
15 skillToProfitQueue.set(skillRequirement, new MaxPriorityQueue());
16 }
17 skillToProfitQueue.get(skillRequirement).enqueue(profit);
18 }
19
20 // Track total profit accumulated
21 let totalProfit = 0;
22
23 // Assign each worker to their best matching task
24 for (const workerSkill of workers) {
25 const profitQueue = skillToProfitQueue.get(workerSkill);
26 if (profitQueue) {
27 // Assign worker to highest profit task at their skill level
28 totalProfit += profitQueue.dequeue();
29
30 // Remove skill entry if no more tasks remain
31 if (profitQueue.size() === 0) {
32 skillToProfitQueue.delete(workerSkill);
33 }
34 }
35 }
36
37 // Find the maximum profit among remaining unassigned tasks
38 let maxRemainingProfit = 0;
39 for (const profitQueue of skillToProfitQueue.values()) {
40 maxRemainingProfit = Math.max(maxRemainingProfit, profitQueue.front());
41 }
42
43 // Add the best remaining task profit to total
44 totalProfit += maxRemainingProfit;
45
46 return totalProfit;
47}
48
Time and Space Complexity
Time Complexity: O((n + m) × log m)
Breaking down the time complexity:
- Building the dictionary with SortedList: For each task, we add the profit to a SortedList. Adding to a SortedList takes
O(log k)
wherek
is the size of that particular list. In the worst case, all tasks have the same skill level, so we performm
insertions with complexityO(log 1) + O(log 2) + ... + O(log m) = O(m log m)
. - Processing workers: For each of the
n
workers, we check ifd[skill]
exists (O(1)) and potentially pop from the SortedList, which takesO(log k)
wherek
is the size of that list. In the worst case, this isO(n log m)
. - Finding maximum remaining profit: We iterate through all skill levels in the dictionary (at most
m
skill levels) and for each non-empty list, we access the last element inO(1)
time. This takesO(m)
. - Overall:
O(m log m) + O(n log m) + O(m) = O((n + m) × log m)
Space Complexity: O(m)
The space is used for:
- The dictionary
d
which stores at mostm
task profits across all SortedLists - Each task is stored exactly once in the data structure
- Therefore, the total space complexity is
O(m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Special Worker's Role
The Problem: A common misunderstanding is thinking the special worker is optional or that they're just another regular worker. The problem states there's "an additional special worker who joins today" - this worker always exists and should always be assigned a task if any tasks remain.
Incorrect Approach:
# Wrong: Only adding special worker's task if there are unmatched workers
if len(workers) < len(tasks):
max_remaining = find_max_remaining_task()
total += max_remaining
Correct Approach: The special worker always exists and takes the best remaining task after regular workers are assigned:
# Correct: Always find and add the best remaining task for special worker
max_remaining_profit = 0
for profit_list in skill_to_profits.values():
if profit_list:
max_remaining_profit = max(max_remaining_profit, profit_list[-1])
total_profit += max_remaining_profit # Always add this, even if it's 0
Pitfall 2: Greedy Global Assignment Instead of Skill-Constrained Assignment
The Problem: You might be tempted to sort all tasks by profit and greedily assign the highest profit tasks first, checking skill compatibility. This doesn't work because regular workers have strict skill requirements.
Incorrect Approach:
# Wrong: Trying to assign highest profit tasks first globally
tasks.sort(key=lambda x: x[1], reverse=True)
assigned_workers = set()
total = 0
for skill_req, profit in tasks:
for i, worker_skill in enumerate(workers):
if i not in assigned_workers and worker_skill == skill_req:
total += profit
assigned_workers.add(i)
break
This fails because a high-profit task might "block" a worker from taking their only compatible task, leaving profitable tasks unassigned.
Correct Approach: Each regular worker should take the best task available for their specific skill level:
# Correct: Each worker takes the best task matching their skill for worker_skill in workers: if skill_to_profits[worker_skill]: total_profit += skill_to_profits[worker_skill].pop()
Pitfall 3: Not Handling Empty Task Lists
The Problem: If there are no tasks at all, or no remaining tasks after regular workers are assigned, the code should handle this gracefully.
Incorrect Approach:
# Wrong: Assumes there's always at least one task remaining
max_remaining = max(profit_list[-1] for profit_list in skill_to_profits.values() if profit_list)
This raises a ValueError
if all profit lists are empty.
Correct Approach: Initialize the maximum to 0 and only update if tasks exist:
# Correct: Handles case where no tasks remain
max_remaining_profit = 0
for profit_list in skill_to_profits.values():
if profit_list:
max_remaining_profit = max(max_remaining_profit, profit_list[-1])
Pitfall 4: Modifying Dictionary While Iterating
The Problem: If you try to clean up empty lists from the dictionary while iterating through workers, you might encounter runtime errors.
Incorrect Approach:
# Wrong: Modifying dictionary structure during iteration for worker_skill in workers: if skill_to_profits[worker_skill]: total += skill_to_profits[worker_skill].pop() if not skill_to_profits[worker_skill]: del skill_to_profits[worker_skill] # Dangerous during iteration
Correct Approach: Leave empty lists in place - they don't affect the final result:
# Correct: Leave empty lists, they're harmless for worker_skill in workers: if skill_to_profits[worker_skill]: total_profit += skill_to_profits[worker_skill].pop() # Empty lists will be skipped when finding max for special worker
What are the most two important steps in writing a depth first search function? (Select 2)
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 assets algo monster 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 is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!