Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 first x tasks after sorting)
  • Use the strongest x workers (the last x 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] to workers[m-1]
  • Process workers from weakest to strongest among these selected workers
  • i tracks the next task to consider
  • q is a deque storing currently available tasks for worker j
  • 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 Evaluator

Example 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
  • 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
  • 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 = 0Return False

✗ 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:

  1. 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
  2. This allowed the two workers with strength 6 to complete the remaining tasks without pills
  3. 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) where n is the number of tasks
  • Sorting workers array: O(m × log m) where m 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 runs x times (at most min(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) where x ≤ min(n, m)
  • 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 the check function can hold at most x elements, where x ≤ 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More