2071. Maximum Number of Tasks You Can Assign
Problem Description
You have n
tasks and m
workers. Each task requires a certain strength to complete, stored in array tasks
, where tasks[i]
is the strength needed for the i-th task. Each worker has a strength value stored in array workers
, where workers[j]
is the strength of the j-th worker.
To complete a task, a worker must have strength greater than or equal to the task's requirement. Each worker can only be assigned to one task at most.
You also have a limited number of magical pills (given by pills
). Each pill increases a worker's strength by exactly strength
points. Each worker can take at most one pill.
The goal is to find the maximum number of tasks that can be completed by optimally:
- Assigning workers to tasks (each worker can do at most one task)
- Distributing pills to workers (each worker can take at most one pill)
For example, if you have:
tasks = [3, 2, 1]
(three tasks requiring strengths 3, 2, and 1)workers = [0, 3, 3]
(three workers with strengths 0, 3, and 3)pills = 1
(one pill available)strength = 1
(each pill increases strength by 1)
You could give the pill to the first worker (strength becomes 0+1=1), allowing them to complete the task requiring strength 1. The other two workers with strength 3 can complete the tasks requiring strengths 2 and 3. Thus, all 3 tasks can be completed.
The function should return the maximum number of tasks that can be completed given these constraints.
Intuition
The key insight is that if we can complete x
tasks, then we can definitely complete x-1
, x-2
, ..., 1
, or 0
tasks. This monotonic property suggests we can use binary search to find the maximum number of tasks we can complete.
First, let's sort both arrays. Sorting tasks
in ascending order and workers
in ascending order helps us make greedy decisions. When trying to complete x
tasks, we should:
- Use the easiest
x
tasks (the firstx
tasks after sorting) - Use the strongest
x
workers (the lastx
workers after sorting)
This gives us the best chance of success because we're matching the easiest tasks with our strongest workers.
For the greedy strategy within each check: when we iterate through our selected workers from weakest to strongest, each worker should consider all tasks they could potentially complete (with or without a pill). Among these available tasks:
- If the worker can complete the easiest task without a pill, do it without using a pill (save pills for workers who really need them)
- If the worker needs a pill to complete any task, use the pill to complete the hardest possible task (this maximizes the value we get from the pill)
Why does this greedy approach work? Consider a worker who needs a pill. If we use the pill on this worker to complete an easier task when they could complete a harder one, we might later find that another worker can't complete that harder task even with a pill. By having each worker who uses a pill complete the hardest task they can, we leave easier tasks for subsequent workers who might be able to complete them without pills or with less "waste" of pill strength.
The deque structure helps efficiently track which tasks are available for the current worker - we can add newly available tasks to the back and remove completed tasks from either end depending on whether we used a pill or not.
Learn more about Greedy, Queue, Two Pointers, Binary Search, Sorting and Monotonic Queue patterns.
Solution Approach
The solution uses binary search combined with a greedy assignment strategy.
Step 1: Sorting
tasks.sort() workers.sort()
Sort both arrays in ascending order to enable greedy decisions.
Step 2: Binary Search Setup
left, right = 0, min(n, m)
We can complete at most min(n, m)
tasks (limited by either the number of tasks or workers). Binary search finds the maximum number of tasks we can complete.
Step 3: Binary Search Loop
while left < right: mid = (left + right + 1) >> 1 if check(mid): left = mid else: right = mid - 1
If we can complete mid
tasks, try a larger number. Otherwise, try a smaller number.
Step 4: The check(x)
Function
This function determines if we can complete exactly x
tasks using a greedy strategy:
def check(x):
i = 0
q = deque()
p = pills
for j in range(m - x, m):
- Use the strongest
x
workers:workers[m-x]
toworkers[m-1]
- Process workers from weakest to strongest among these selected workers
i
tracks the next task to considerq
is a deque storing currently available tasks for workerj
p
tracks remaining pills
For each worker j
:
while i < x and tasks[i] <= workers[j] + strength: q.append(tasks[i]) i += 1
Add all tasks that this worker could potentially complete (with a pill) to the deque.
if not q: return False
If no tasks are available for this worker, it's impossible to complete x
tasks.
if q[0] <= workers[j]: q.popleft()
If the worker can complete the easiest available task without a pill, do so. Remove from the front of the deque (smallest task).
elif p == 0: return False else: p -= 1 q.pop()
Otherwise, the worker needs a pill. If no pills remain, return false
. If pills are available, use one and complete the hardest available task (remove from the back of the deque).
The greedy logic ensures:
- Workers who don't need pills complete easier tasks
- Workers who need pills complete harder tasks to maximize pill value
- Pills are used only when necessary
The algorithm runs in O((n + m) log(min(n, m)) * min(n, m))
time complexity due to the binary search and the linear check function.
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 understand how the algorithm works:
Given:
tasks = [5, 9, 8, 5, 9]
workers = [1, 6, 4, 2, 6]
pills = 1
strength = 5
Step 1: Sort both arrays
tasks = [5, 5, 8, 9, 9]
workers = [1, 2, 4, 6, 6]
Step 2: Binary Search
We can complete at most min(5, 5) = 5
tasks. Binary search range: [0, 5]
First iteration: mid = 3
(check if we can complete 3 tasks)
- Select easiest 3 tasks:
[5, 5, 8]
- Select strongest 3 workers:
[4, 6, 6]
Processing workers from weakest to strongest:
-
Worker with strength 4:
- Can potentially complete tasks
[5, 5, 8]
with pill (4+5=9) - Add all to deque:
q = [5, 5, 8]
- Can't complete task 5 without pill (4 < 5)
- Use pill → strength becomes 9 → complete hardest task (8)
- Remove 8 from back:
q = [5, 5]
,pills = 0
- Can potentially complete tasks
-
Worker with strength 6:
- No new tasks to add (all were already added)
q = [5, 5]
- Can complete task 5 without pill (6 ≥ 5)
- Remove 5 from front:
q = [5]
-
Worker with strength 6:
- No new tasks to add
q = [5]
- Can complete task 5 without pill (6 ≥ 5)
- Remove 5 from front:
q = []
✓ Successfully completed 3 tasks! Update: left = 3
Second iteration: mid = 4
(check if we can complete 4 tasks)
- Select easiest 4 tasks:
[5, 5, 8, 9]
- Select strongest 4 workers:
[2, 4, 6, 6]
Processing workers:
-
Worker with strength 2:
- Can potentially complete tasks
[5, 5]
with pill (2+5=7) - Add to deque:
q = [5, 5]
- Can't complete task 5 without pill (2 < 5)
- Use pill → strength becomes 7 → complete hardest task (5)
- Remove 5 from back:
q = [5]
,pills = 0
- Can potentially complete tasks
-
Worker with strength 4:
- Can potentially complete task
[8]
with pill (4+5=9) - Add to deque:
q = [5, 8]
- Can't complete task 5 without pill (4 < 5)
- Need pill but
pills = 0
→ Return False
- Can potentially complete task
✗ Cannot complete 4 tasks! Update: right = 3
Binary search converges: left = right = 3
Answer: 3 tasks can be completed
The key insight illustrated here is how the greedy strategy works:
- When the worker with strength 4 used the pill in the 3-task scenario, they wisely chose the hardest task (8), leaving easier tasks for stronger workers
- This allowed the two workers with strength 6 to complete the remaining tasks without pills
- When we tried 4 tasks, we ran out of pills too early because weaker workers needed them, demonstrating why the greedy pill usage (complete hardest possible task when using a pill) is optimal
Solution Implementation
1class Solution:
2 def maxTaskAssign(
3 self, tasks: List[int], workers: List[int], pills: int, strength: int
4 ) -> int:
5 def can_assign_k_tasks(k: int) -> bool:
6 """
7 Check if we can assign k tasks to the strongest k workers.
8
9 Strategy: Use the strongest k workers and try to complete the easiest k tasks.
10 For each worker (from weakest to strongest among the selected k):
11 - Add all tasks that this worker could complete with a pill to the queue
12 - If worker can complete the easiest task without pill, do it
13 - Otherwise, use a pill on the hardest task in queue (greedy choice)
14 """
15 task_index = 0
16 available_tasks = deque()
17 pills_remaining = pills
18
19 # Iterate through the strongest k workers (from weakest to strongest)
20 for worker_index in range(num_workers - k, num_workers):
21 current_worker_strength = workers[worker_index]
22
23 # Add all tasks that this worker could potentially complete (with pill)
24 # to the available tasks queue
25 while task_index < k and tasks[task_index] <= current_worker_strength + strength:
26 available_tasks.append(tasks[task_index])
27 task_index += 1
28
29 # No tasks available for this worker
30 if not available_tasks:
31 return False
32
33 # If worker can complete the easiest task without a pill
34 if available_tasks[0] <= current_worker_strength:
35 available_tasks.popleft()
36 # Need a pill but none available
37 elif pills_remaining == 0:
38 return False
39 # Use a pill on the hardest available task
40 else:
41 pills_remaining -= 1
42 available_tasks.pop() # Remove the hardest task from the right
43
44 return True
45
46 # Initialize variables
47 num_tasks = len(tasks)
48 num_workers = len(workers)
49
50 # Sort both arrays for greedy assignment
51 tasks.sort()
52 workers.sort()
53
54 # Binary search for the maximum number of tasks we can complete
55 left = 0
56 right = min(num_tasks, num_workers)
57
58 while left < right:
59 # Use ceiling division to bias towards the upper half
60 mid = (left + right + 1) // 2
61
62 if can_assign_k_tasks(mid):
63 # Can assign mid tasks, try for more
64 left = mid
65 else:
66 # Cannot assign mid tasks, try fewer
67 right = mid - 1
68
69 return left
70
1class Solution {
2 // Instance variables to store problem parameters
3 private int[] tasks;
4 private int[] workers;
5 private int strength;
6 private int pills;
7 private int tasksLength;
8 private int workersLength;
9
10 /**
11 * Finds the maximum number of tasks that can be completed by workers.
12 * Workers can optionally take pills to increase their strength.
13 *
14 * @param tasks Array of task difficulties
15 * @param workers Array of worker strengths
16 * @param pills Number of available strength-enhancing pills
17 * @param strength Amount of strength increase from each pill
18 * @return Maximum number of tasks that can be completed
19 */
20 public int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
21 // Sort both arrays to enable efficient matching
22 Arrays.sort(tasks);
23 Arrays.sort(workers);
24
25 // Initialize instance variables
26 this.tasks = tasks;
27 this.workers = workers;
28 this.strength = strength;
29 this.pills = pills;
30 this.tasksLength = tasks.length;
31 this.workersLength = workers.length;
32
33 // Binary search for the maximum number of tasks that can be completed
34 int left = 0;
35 int right = Math.min(workersLength, tasksLength);
36
37 while (left < right) {
38 // Calculate mid point (biased towards right for upper bound search)
39 int mid = (left + right + 1) >> 1;
40
41 if (canCompleteKTasks(mid)) {
42 // If we can complete mid tasks, try for more
43 left = mid;
44 } else {
45 // If we cannot complete mid tasks, try for fewer
46 right = mid - 1;
47 }
48 }
49
50 return left;
51 }
52
53 /**
54 * Checks if it's possible to complete exactly k tasks.
55 * Uses a greedy approach: assign strongest k workers to easiest k tasks.
56 *
57 * @param k Number of tasks to attempt completing
58 * @return true if k tasks can be completed, false otherwise
59 */
60 private boolean canCompleteKTasks(int k) {
61 int taskIndex = 0;
62 Deque<Integer> availableTasks = new ArrayDeque<>();
63 int remainingPills = pills;
64
65 // Iterate through the k strongest workers (from right side of sorted array)
66 for (int workerIndex = workersLength - k; workerIndex < workersLength; workerIndex++) {
67 // Add all tasks that this worker could complete with a pill
68 // to the available tasks queue
69 while (taskIndex < k && tasks[taskIndex] <= workers[workerIndex] + strength) {
70 availableTasks.offer(tasks[taskIndex]);
71 taskIndex++;
72 }
73
74 // If no tasks are available for this worker, assignment fails
75 if (availableTasks.isEmpty()) {
76 return false;
77 }
78
79 // Try to assign the easiest task without using a pill
80 if (availableTasks.peekFirst() <= workers[workerIndex]) {
81 availableTasks.pollFirst();
82 }
83 // If the easiest task requires a pill, check if pills are available
84 else if (remainingPills == 0) {
85 return false;
86 }
87 // Use a pill and assign the hardest available task
88 // (greedy: save easier tasks for weaker workers)
89 else {
90 remainingPills--;
91 availableTasks.pollLast();
92 }
93 }
94
95 return true;
96 }
97}
98
1class Solution {
2public:
3 int maxTaskAssign(vector<int>& tasks, vector<int>& workers, int pills, int strength) {
4 // Sort both arrays to enable greedy assignment
5 sort(tasks.begin(), tasks.end());
6 sort(workers.begin(), workers.end());
7
8 int numTasks = tasks.size();
9 int numWorkers = workers.size();
10
11 // Binary search on the number of tasks that can be completed
12 int left = 0;
13 int right = min(numWorkers, numTasks);
14
15 // Lambda function to check if we can complete 'targetTasks' number of tasks
16 auto canCompleteTargetTasks = [&](int targetTasks) -> bool {
17 int remainingPills = pills;
18 deque<int> availableTasks; // Tasks that can potentially be done (with or without pill)
19 int taskIndex = 0;
20
21 // Try to assign the strongest workers to complete targetTasks
22 // Start from the strongest workers (last targetTasks workers)
23 for (int workerIndex = numWorkers - targetTasks; workerIndex < numWorkers; ++workerIndex) {
24 // Add all tasks that this worker can do with a pill to the deque
25 while (taskIndex < targetTasks && tasks[taskIndex] <= workers[workerIndex] + strength) {
26 availableTasks.push_back(tasks[taskIndex]);
27 taskIndex++;
28 }
29
30 // If no tasks available for this worker, assignment fails
31 if (availableTasks.empty()) {
32 return false;
33 }
34
35 // Try to assign the easiest task without using a pill
36 if (availableTasks.front() <= workers[workerIndex]) {
37 availableTasks.pop_front();
38 }
39 // Otherwise, use a pill to complete the hardest available task
40 else if (remainingPills == 0) {
41 return false; // No pills left, cannot complete assignment
42 }
43 else {
44 remainingPills--;
45 availableTasks.pop_back(); // Remove the hardest task (requires pill)
46 }
47 }
48
49 return true;
50 };
51
52 // Binary search to find maximum number of tasks that can be completed
53 while (left < right) {
54 int mid = (left + right + 1) / 2; // Upper mid to avoid infinite loop
55
56 if (canCompleteTargetTasks(mid)) {
57 left = mid; // Can complete mid tasks, try more
58 } else {
59 right = mid - 1; // Cannot complete mid tasks, try fewer
60 }
61 }
62
63 return left;
64 }
65};
66
1/**
2 * Assigns maximum number of tasks to workers with optional strength pills
3 * @param tasks - Array of task difficulties
4 * @param workers - Array of worker strengths
5 * @param pills - Number of strength pills available
6 * @param strength - Strength boost provided by each pill
7 * @returns Maximum number of tasks that can be completed
8 */
9function maxTaskAssign(
10 tasks: number[],
11 workers: number[],
12 pills: number,
13 strength: number,
14): number {
15 // Sort both arrays in ascending order
16 tasks.sort((a, b) => a - b);
17 workers.sort((a, b) => a - b);
18
19 const tasksCount = tasks.length;
20 const workersCount = workers.length;
21
22 /**
23 * Checks if it's possible to complete 'targetTasks' number of tasks
24 * @param targetTasks - Number of tasks to attempt completing
25 * @returns true if targetTasks can be completed, false otherwise
26 */
27 const canCompleteTargetTasks = (targetTasks: number): boolean => {
28 // Deque to store assignable tasks for current configuration
29 const taskDeque = new Array<number>(targetTasks);
30 let dequeHead = 0;
31 let dequeTail = 0;
32
33 // Deque helper functions
34 const isEmpty = (): boolean => dequeHead === dequeTail;
35 const pushBack = (value: number): void => {
36 taskDeque[dequeTail++] = value;
37 };
38 const popFront = (): void => {
39 dequeHead++;
40 };
41 const popBack = (): void => {
42 dequeTail--;
43 };
44 const getFront = (): number => taskDeque[dequeHead];
45
46 let taskIndex = 0;
47 let remainingPills = pills;
48
49 // Try to assign tasks to the strongest 'targetTasks' workers
50 for (let workerIndex = workersCount - targetTasks; workerIndex < workersCount; workerIndex++) {
51 // Add all tasks that this worker could potentially complete (with pill)
52 while (taskIndex < targetTasks && tasks[taskIndex] <= workers[workerIndex] + strength) {
53 pushBack(tasks[taskIndex]);
54 taskIndex++;
55 }
56
57 // No assignable tasks for this worker
58 if (isEmpty()) {
59 return false;
60 }
61
62 // Check if worker can complete the easiest available task without pill
63 if (getFront() <= workers[workerIndex]) {
64 // Assign easiest task without using a pill
65 popFront();
66 } else {
67 // Worker needs a pill to complete any task
68 if (remainingPills === 0) {
69 return false;
70 }
71 // Use a pill and assign the hardest possible task
72 remainingPills--;
73 popBack();
74 }
75 }
76
77 return true;
78 };
79
80 // Binary search for the maximum number of completable tasks
81 let left = 0;
82 let right = Math.min(tasksCount, workersCount);
83
84 while (left < right) {
85 // Calculate mid point (ceiling division for upper bound binary search)
86 const mid = (left + right + 1) >> 1;
87
88 if (canCompleteTargetTasks(mid)) {
89 // Can complete 'mid' tasks, try for more
90 left = mid;
91 } else {
92 // Cannot complete 'mid' tasks, try fewer
93 right = mid - 1;
94 }
95 }
96
97 return left;
98}
99
Time and Space Complexity
Time Complexity: O((n + m) × log(n + m) + min(n, m) × n × log(min(n, m)))
The time complexity breaks down as follows:
- Sorting tasks array:
O(n × log n)
wheren
is the number of tasks - Sorting workers array:
O(m × log m)
wherem
is the number of workers - Binary search runs
O(log(min(n, m)))
iterations - For each binary search iteration, the
check
function is called:- The outer loop in
check
runsx
times (at mostmin(n, m)
) - The inner while loop processes at most
x
tasks total across all iterations - Deque operations (append, popleft, pop) are
O(1)
- Overall
check
function:O(x)
wherex ≤ min(n, m)
- The outer loop in
- Total for binary search with check:
O(min(n, m) × log(min(n, m)))
Since sorting dominates when n
and m
are large, and considering the reference answer simplifies to O(n × log n)
, this suggests m
is treated as comparable to n
. The dominant term becomes O(n × log n)
.
Space Complexity: O(n)
The space complexity consists of:
- The deque
q
in thecheck
function can hold at mostx
elements, wherex ≤ min(n, m) ≤ n
- Other variables use constant space
- Total space:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Worker Selection Strategy
Pitfall: A common mistake is trying to use the weakest workers first or randomly selecting workers, thinking we should "save" stronger workers for harder tasks.
Why it's wrong: This approach fails because:
- Weak workers might not be able to complete any tasks even with pills
- We waste pills on workers who can't contribute effectively
- We miss opportunities where strong workers could complete tasks without pills
Example:
# WRONG: Using weakest k workers
for worker_index in range(k): # Starts from weakest workers
current_worker_strength = workers[worker_index]
# ... rest of logic
Solution: Always use the strongest k workers when trying to complete k tasks:
# CORRECT: Use strongest k workers
for worker_index in range(num_workers - k, num_workers):
current_worker_strength = workers[worker_index]
# ... rest of logic
2. Greedy Pill Usage on Wrong Tasks
Pitfall: When a worker needs a pill, assigning them to the easiest available task (from the front of the deque).
Why it's wrong: This wastes the pill's potential. If we use a pill, we should maximize its value by completing the hardest possible task.
Example:
# WRONG: Using pill on easiest task if available_tasks[0] <= current_worker_strength: available_tasks.popleft() elif pills_remaining > 0: pills_remaining -= 1 available_tasks.popleft() # Wrong! Taking easiest task with pill
Solution: When using a pill, assign the worker to the hardest available task:
# CORRECT: Using pill on hardest task if available_tasks[0] <= current_worker_strength: available_tasks.popleft() # No pill needed, take easiest elif pills_remaining > 0: pills_remaining -= 1 available_tasks.pop() # With pill, take hardest task
3. Incorrect Task Availability Window
Pitfall: Adding tasks to the available queue based only on the worker's base strength, or adding all tasks at once.
Why it's wrong:
- We need to consider tasks that the worker could complete WITH a pill
- But we shouldn't add tasks that are too hard even with a pill
- Adding tasks incrementally ensures proper ordering for the greedy strategy
Example:
# WRONG: Only considering base strength while task_index < k and tasks[task_index] <= current_worker_strength: available_tasks.append(tasks[task_index]) task_index += 1
Solution: Consider the worker's strength with a potential pill:
# CORRECT: Consider strength with pill while task_index < k and tasks[task_index] <= current_worker_strength + strength: available_tasks.append(tasks[task_index]) task_index += 1
4. Binary Search Boundary Error
Pitfall: Using standard binary search mid calculation which can cause infinite loops or miss the answer.
Example:
# WRONG: Can cause infinite loop when left + 1 == right while left < right: mid = (left + right) // 2 # Floor division if can_assign_k_tasks(mid): left = mid # This can cause infinite loop else: right = mid - 1
Solution: Use ceiling division for mid when updating left = mid:
# CORRECT: Ceiling division prevents infinite loop while left < right: mid = (left + right + 1) // 2 # Ceiling division if can_assign_k_tasks(mid): left = mid # Safe now else: right = mid - 1
These pitfalls often lead to incorrect results or runtime errors. The key insight is that the greedy strategy must be carefully implemented: use the strongest workers, and when pills are needed, maximize their value by completing the hardest possible tasks.
Which algorithm should you use to find a node that is close to the root of the tree?
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
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Want a Structured Path to Master System Design Too? Don’t Miss This!