826. Most Profit Assigning Work
Problem Description
You are given information about n
jobs and m
workers, represented by three arrays:
difficulty[i]
: the difficulty level of thei
-th jobprofit[i]
: the profit earned from completing thei
-th jobworker[j]
: the maximum difficulty level that thej
-th worker can handle
The key rules are:
-
Worker Capability: A worker can only complete jobs whose difficulty is at most their ability level. For example, if
worker[j] = 5
, they can only do jobs withdifficulty ≤ 5
. -
Assignment Constraints: Each worker can be assigned to at most one job (or no job at all).
-
Job Reusability: The same job can be completed by multiple workers. If three workers each complete a job worth
$1
, the total profit is$3
. -
Zero Profit: If a worker cannot complete any available job (all jobs are too difficult), they contribute
$0
to the total profit.
Your task is to assign workers to jobs in a way that maximizes the total profit. Each worker should be assigned to the most profitable job they are capable of completing.
For example, if you have:
- Jobs with
difficulty = [2, 4, 6]
andprofit = [10, 20, 30]
- Workers with abilities
[4, 5, 3]
The optimal assignment would be:
- Worker with ability 4 → job with difficulty 4 (profit 20)
- Worker with ability 5 → job with difficulty 4 (profit 20)
- Worker with ability 3 → job with difficulty 2 (profit 10)
- Total profit = 50
Intuition
The key insight is that each worker should take the most profitable job they're capable of doing. Since multiple workers can do the same job, we don't need to worry about "reserving" jobs for specific workers.
Think of it this way: if a worker can handle difficulty level 5, they can do any job with difficulty ≤ 5
. Among all these jobs they're capable of doing, why would they choose anything other than the one with maximum profit?
This leads us to a greedy approach: for each worker, find the highest-paying job within their capability.
To implement this efficiently, we can:
-
Sort the jobs by difficulty: This allows us to quickly find all jobs a worker can do (all jobs up to a certain difficulty threshold).
-
Sort the workers by ability: This lets us process workers in order, and reuse our progress. If worker A has ability 3 and we've already found the best job for them, when we move to worker B with ability 5, we know they can do everything worker A can do, plus potentially more difficult jobs.
-
Use two pointers: As we process each worker in ascending order of ability, we can maintain a pointer in the jobs array. For each worker, we advance this pointer to include all new jobs they can handle, keeping track of the maximum profit seen so far.
The beauty of this approach is that once we've found the maximum profit available for jobs with difficulty ≤ k
, we can reuse this information for any worker with ability ≥ k
. This avoids redundant searching and gives us an efficient O(n log n + m log m)
solution where n
is the number of jobs and m
is the number of workers.
Learn more about Greedy, Two Pointers, Binary Search and Sorting patterns.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Sort the workers and create job pairs
worker.sort()
jobs = sorted(zip(difficulty, profit))
- We sort the
worker
array in ascending order of ability - We combine
difficulty
andprofit
into pairs and sort them by difficulty. This creates a list of tuples like[(difficulty1, profit1), (difficulty2, profit2), ...]
sorted by the first element (difficulty)
Step 2: Initialize tracking variables
ans = mx = i = 0
ans
: accumulates the total profitmx
: tracks the maximum profit available for jobs we've seen so fari
: pointer to track our position in the sorted jobs array
Step 3: Process each worker using two pointers
for w in worker:
while i < len(jobs) and jobs[i][0] <= w:
mx = max(mx, jobs[i][1])
i += 1
ans += mx
For each worker with ability w
:
- We advance pointer
i
through the jobs array, considering all jobs wherejobs[i][0] <= w
(difficulty ≤ worker's ability) - For each new job this worker can handle, we update
mx
to track the maximum profit seen:mx = max(mx, jobs[i][1])
- After checking all newly accessible jobs, we add
mx
to our answer - this worker takes the best job available to them - Importantly, we don't reset
i
for the next worker. Since workers are sorted by ability, the next worker can do everything the current worker can do, plus potentially more
Why this works:
- Since both arrays are sorted, when we move from a worker with ability
k
to one with abilityk+1
, we only need to check the additional jobs that become available (those with difficulty betweenk
andk+1
) - The variable
mx
maintains the best profit among all jobs with difficulty ≤ current worker's ability - Each worker gets assigned the job with maximum profit they can handle (stored in
mx
)
Time Complexity: O(n log n + m log m)
for sorting, plus O(n + m)
for the two-pointer traversal
Space Complexity: O(n)
for the jobs array
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's work through a concrete example to illustrate the solution approach.
Given:
difficulty = [5, 2, 4, 6, 8]
profit = [10, 15, 30, 25, 40]
worker = [4, 7, 3, 6]
Step 1: Sort workers and create job pairs
After sorting:
worker = [3, 4, 6, 7]
(sorted by ability)jobs = [(2, 15), (4, 30), (5, 10), (6, 25), (8, 40)]
(sorted by difficulty)
Step 2: Initialize variables
ans = 0
(total profit)mx = 0
(max profit seen so far)i = 0
(pointer in jobs array)
Step 3: Process each worker
Worker 1 (ability = 3):
- Check jobs where difficulty ≤ 3
jobs[0] = (2, 15)
: difficulty 2 ≤ 3 ✓- Update
mx = max(0, 15) = 15
- Move
i
to 1
- Update
jobs[1] = (4, 30)
: difficulty 4 > 3 ✗ (stop)- Add
mx = 15
to answer ans = 15
,i = 1
,mx = 15
Worker 2 (ability = 4):
- Continue from where we left off (
i = 1
) - Check additional jobs where difficulty ≤ 4
jobs[1] = (4, 30)
: difficulty 4 ≤ 4 ✓- Update
mx = max(15, 30) = 30
- Move
i
to 2
- Update
jobs[2] = (5, 10)
: difficulty 5 > 4 ✗ (stop)- Add
mx = 30
to answer ans = 45
,i = 2
,mx = 30
Worker 3 (ability = 6):
- Continue from
i = 2
- Check additional jobs where difficulty ≤ 6
jobs[2] = (5, 10)
: difficulty 5 ≤ 6 ✓- Update
mx = max(30, 10) = 30
(no change) - Move
i
to 3
- Update
jobs[3] = (6, 25)
: difficulty 6 ≤ 6 ✓- Update
mx = max(30, 25) = 30
(no change) - Move
i
to 4
- Update
jobs[4] = (8, 40)
: difficulty 8 > 6 ✗ (stop)- Add
mx = 30
to answer ans = 75
,i = 4
,mx = 30
Worker 4 (ability = 7):
- Continue from
i = 4
- Check additional jobs where difficulty ≤ 7
i = 4
is already at the end of jobs array (no more jobs to check)- The best job this worker can do is still the one with profit 30
- Add
mx = 30
to answer ans = 105
Final Result: 105
Each worker was assigned to their optimal job:
- Worker (ability 3) → Job (difficulty 2, profit 15)
- Worker (ability 4) → Job (difficulty 4, profit 30)
- Worker (ability 6) → Job (difficulty 4, profit 30)
- Worker (ability 7) → Job (difficulty 4, profit 30)
Notice how the two-pointer technique allows us to efficiently find the best job for each worker without repeatedly searching through all jobs. The key insight is that once we've identified the maximum profit for jobs up to a certain difficulty, we can reuse this information for all subsequent workers with higher abilities.
Solution Implementation
1class Solution:
2 def maxProfitAssignment(
3 self, difficulty: List[int], profit: List[int], worker: List[int]
4 ) -> int:
5 # Sort workers by their ability in ascending order
6 worker.sort()
7
8 # Create a list of (difficulty, profit) tuples and sort by difficulty
9 jobs = sorted(zip(difficulty, profit))
10
11 # Initialize variables
12 total_profit = 0 # Total profit accumulated from all workers
13 max_profit_so_far = 0 # Maximum profit available for current difficulty level
14 job_index = 0 # Index to track which jobs have been considered
15
16 # Process each worker in order of increasing ability
17 for worker_ability in worker:
18 # Find the most profitable job this worker can do
19 # Check all jobs with difficulty <= worker's ability
20 while job_index < len(jobs) and jobs[job_index][0] <= worker_ability:
21 # Update the maximum profit seen so far
22 max_profit_so_far = max(max_profit_so_far, jobs[job_index][1])
23 job_index += 1
24
25 # Assign the best available job (highest profit) to this worker
26 total_profit += max_profit_so_far
27
28 return total_profit
29
1class Solution {
2 public int maxProfitAssignment(int[] difficulty, int[] profit, int[] worker) {
3 // Sort workers by their ability in ascending order
4 Arrays.sort(worker);
5
6 // Get the number of jobs
7 int numberOfJobs = profit.length;
8
9 // Create a 2D array to store job pairs (difficulty, profit)
10 int[][] jobs = new int[numberOfJobs][2];
11 for (int i = 0; i < numberOfJobs; i++) {
12 jobs[i] = new int[] {difficulty[i], profit[i]};
13 }
14
15 // Sort jobs by difficulty in ascending order
16 Arrays.sort(jobs, (a, b) -> a[0] - b[0]);
17
18 // Initialize variables for tracking total profit and maximum profit seen so far
19 int totalProfit = 0;
20 int maxProfitSoFar = 0;
21 int jobIndex = 0;
22
23 // Process each worker
24 for (int workerAbility : worker) {
25 // Find all jobs this worker can do and track the maximum profit
26 while (jobIndex < numberOfJobs && jobs[jobIndex][0] <= workerAbility) {
27 maxProfitSoFar = Math.max(maxProfitSoFar, jobs[jobIndex][1]);
28 jobIndex++;
29 }
30
31 // Assign the best job (with maximum profit) to this worker
32 totalProfit += maxProfitSoFar;
33 }
34
35 return totalProfit;
36 }
37}
38
1class Solution {
2public:
3 int maxProfitAssignment(vector<int>& difficulty, vector<int>& profit, vector<int>& worker) {
4 // Sort workers by their ability in ascending order
5 sort(worker.begin(), worker.end());
6
7 // Get the number of jobs
8 int numJobs = profit.size();
9
10 // Create pairs of (difficulty, profit) for each job
11 vector<pair<int, int>> jobs;
12 for (int i = 0; i < numJobs; ++i) {
13 jobs.emplace_back(difficulty[i], profit[i]);
14 }
15
16 // Sort jobs by difficulty in ascending order
17 sort(jobs.begin(), jobs.end());
18
19 // Initialize variables for tracking total profit and maximum profit seen so far
20 int totalProfit = 0;
21 int maxProfitSoFar = 0;
22 int jobIndex = 0;
23
24 // Process each worker
25 for (int workerAbility : worker) {
26 // Find the most profitable job this worker can do
27 // Update maxProfitSoFar with all jobs the worker is capable of
28 while (jobIndex < numJobs && jobs[jobIndex].first <= workerAbility) {
29 maxProfitSoFar = max(maxProfitSoFar, jobs[jobIndex].second);
30 jobIndex++;
31 }
32
33 // Add the best profit this worker can achieve to the total
34 totalProfit += maxProfitSoFar;
35 }
36
37 return totalProfit;
38 }
39};
40
1/**
2 * Calculates maximum profit by assigning workers to jobs based on their abilities
3 * @param difficulty - Array of job difficulty levels
4 * @param profit - Array of profits for corresponding jobs
5 * @param worker - Array of worker abilities
6 * @returns Total maximum profit from all worker assignments
7 */
8function maxProfitAssignment(difficulty: number[], profit: number[], worker: number[]): number {
9 const jobCount: number = profit.length;
10
11 // Sort workers by their ability in ascending order
12 worker.sort((a: number, b: number) => a - b);
13
14 // Create job pairs [difficulty, profit] and sort by difficulty
15 const jobs: number[][] = Array.from(
16 { length: jobCount },
17 (_: unknown, index: number) => [difficulty[index], profit[index]]
18 );
19 jobs.sort((jobA: number[], jobB: number[]) => jobA[0] - jobB[0]);
20
21 let totalProfit: number = 0;
22 let maxProfitSoFar: number = 0;
23 let jobIndex: number = 0;
24
25 // For each worker, find the best paying job they can do
26 for (const workerAbility of worker) {
27 // Update maxProfitSoFar with all jobs this worker can handle
28 while (jobIndex < jobCount && jobs[jobIndex][0] <= workerAbility) {
29 maxProfitSoFar = Math.max(maxProfitSoFar, jobs[jobIndex][1]);
30 jobIndex++;
31 }
32
33 // Add the best profit this worker can achieve
34 totalProfit += maxProfitSoFar;
35 }
36
37 return totalProfit;
38}
39
Time and Space Complexity
Time Complexity: O(n × log n + m × log m)
The time complexity breaks down as follows:
- Sorting the
worker
array takesO(m × log m)
wherem
is the length of the worker array - Creating and sorting the
jobs
list (zipping difficulty and profit, then sorting) takesO(n × log n)
wheren
is the length of the difficulty/profit arrays - The main loop iterates through each worker (
m
iterations), and the inner while loop moves through jobs. Although there's a nested loop structure, the indexi
only moves forward and never resets, so each job is visited at most once across all iterations. This contributesO(n + m)
to the complexity - The dominant operations are the sorting steps, giving us a total time complexity of
O(n × log n + m × log m)
Space Complexity: O(n)
The space complexity analysis:
- The
jobs
list stores a sorted version of zipped difficulty and profit pairs, requiringO(n)
space wheren
is the number of jobs - The sorting of
worker
is done in-place (Python's sort modifies the list directly), so no additional space is needed for that operation - Other variables (
ans
,mx
,i
,w
) use constant spaceO(1)
- Therefore, the total space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting that multiple workers can do the same job
The Mistake: A common misconception is thinking each job can only be assigned once, leading to incorrect solutions that try to match workers to unique jobs like a bipartite matching problem.
Why it happens: Many assignment problems have a one-to-one constraint, so it's natural to assume the same here.
Example that breaks:
- Jobs:
difficulty = [1]
,profit = [100]
- Workers:
[5, 5, 5]
- Wrong answer: 100 (thinking only one worker can take the job)
- Correct answer: 300 (all three workers can do the same job)
Solution: Remember that jobs are reusable - track the maximum profit available at each difficulty level, not which specific jobs are "taken."
Pitfall 2: Not considering that a harder job might have less profit
The Mistake: Assuming that higher difficulty always means higher profit and just assigning each worker the hardest job they can do.
Why it happens: It seems intuitive that harder jobs would pay more.
Example that breaks:
- Jobs:
difficulty = [2, 5]
,profit = [50, 10]
- Worker with ability 5
- Wrong answer: 10 (taking the hardest job)
- Correct answer: 50 (taking the easier but more profitable job)
Solution: Always track the maximum profit among ALL jobs a worker can handle, not just the hardest one. This is why the code maintains max_profit_so_far
across all feasible difficulties.
Pitfall 3: Resetting the job index for each worker
The Mistake: Setting job_index = 0
for each worker, causing unnecessary re-scanning of the jobs array.
Why it happens: It seems like each worker needs to check all possible jobs from the beginning.
Example of inefficiency:
for worker_ability in worker:
job_index = 0 # WRONG: This resets for each worker
while job_index < len(jobs) and jobs[job_index][0] <= worker_ability:
# ... process jobs
Solution: Keep the job index persistent across workers. Since workers are sorted by ability, each subsequent worker can do everything the previous worker could do, plus potentially more. The two-pointer technique leverages this monotonic property for O(n + m) traversal instead of O(n * m).
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
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
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!