2462. Total Cost to Hire K Workers
Problem Description
You have an array costs
where costs[i]
represents the cost of hiring worker i
. You need to hire exactly k
workers using a specific selection process.
The hiring process works as follows:
-
Run k hiring sessions - In each session, you'll hire exactly one worker.
-
Selection pool - In each session, you can only choose from:
- The first
candidates
workers from the remaining workers - The last
candidates
workers from the remaining workers
- The first
-
Selection criteria - From your selection pool, always choose:
- The worker with the lowest cost
- If there's a tie in cost, choose the worker with the smallest index
-
Special case - If fewer than
candidates
workers remain, consider all remaining workers for selection. -
One-time selection - Each worker can only be hired once.
Example walkthrough:
- Given
costs = [3,2,7,7,1,2]
andcandidates = 2
- First session: Consider workers at indices
[0,1]
(first 2) and[4,5]
(last 2)- Costs are
[3,2]
and[1,2]
- Minimum cost is 1 at index 4, so hire worker 4
- Costs are
- Second session: Consider workers at indices
[0,1]
(first 2) and[5]
(last 2, but worker 4 already hired)- Costs are
[3,2]
and[2]
- Minimum cost is 2, appearing at both index 1 and 5
- Choose index 1 due to smaller index
- Costs are
The goal is to return the total cost of hiring exactly k
workers following these rules.
Intuition
The key insight is that we're always selecting from two specific groups: the leftmost candidates
workers and the rightmost candidates
workers. This naturally suggests using a data structure that can efficiently give us the minimum element from these two groups.
Think of it like having two windows - one at the front and one at the back of the array. We need to repeatedly pick the cheapest worker from either window. A min heap (priority queue) is perfect for this because it always gives us the minimum element in O(log n)
time.
However, there's an important edge case to consider first: if candidates * 2 >= n
, the two windows overlap or cover the entire array. In this case, we can simply sort all workers by cost and pick the cheapest k
workers - no need for the complex window mechanism.
For the general case, we maintain a min heap containing workers from both windows. After selecting a worker, we need to replenish our selection pool. Here's where the two pointers come in:
- Use pointer
l
to track the next worker to add from the left side - Use pointer
r
to track the next worker to add from the right side
When we remove a worker from the heap, we check which window it came from (by comparing its index with l
). If it's from the left window, we add the next available worker from the left (costs[l]
) and move l
forward. If it's from the right window, we add the next available worker from the right (costs[r]
) and move r
backward.
The process continues until l > r
, meaning the two windows have met and all available workers have been considered. At this point, we just keep selecting from whatever remains in the heap until we've hired k
workers.
The heap automatically handles the tie-breaking rule (smallest index) because when we insert (cost, index)
tuples, Python's heap will use the second element (index) to break ties when costs are equal.
Learn more about Two Pointers and Heap (Priority Queue) patterns.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Handle the edge case
if candidates * 2 >= n:
return sum(sorted(costs)[:k])
When candidates * 2 >= n
, the two windows overlap or cover the entire array. We simply sort all costs and return the sum of the first k
elements.
Step 2: Initialize the min heap with both windows
pq = []
for i, c in enumerate(costs[:candidates]):
heappush(pq, (c, i))
for i in range(n - candidates, n):
heappush(pq, (costs[i], i))
We create a min heap pq
and populate it with:
- The first
candidates
workers (indices 0 tocandidates-1
) - The last
candidates
workers (indicesn-candidates
ton-1
)
Each entry is a tuple (cost, index)
so the heap can sort by cost first, then by index for tie-breaking.
Step 3: Set up pointers for window boundaries
l, r = candidates, n - candidates - 1
l
points to the next worker to potentially add from the left (starts at indexcandidates
)r
points to the next worker to potentially add from the right (starts at indexn - candidates - 1
)
Step 4: Perform k hiring sessions
ans = 0
for _ in range(k):
c, i = heappop(pq)
ans += c
In each iteration, we extract the worker with minimum cost from the heap and add their cost to our answer.
Step 5: Replenish the heap
if l > r: continue if i < l: heappush(pq, (costs[l], l)) l += 1 else: heappush(pq, (costs[r], r)) r -= 1
After removing a worker:
- If
l > r
, the two windows have met, so we don't add any new workers - If the removed worker's index
i < l
, it was from the left window, so we add the next left worker at indexl
and incrementl
- Otherwise, it was from the right window, so we add the next right worker at index
r
and decrementr
This process ensures we always maintain the correct candidates from both ends while gradually working toward the middle of the array. The heap automatically handles finding the minimum cost worker and breaking ties by index.
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 the solution approach:
Input: costs = [17, 12, 10, 2, 7, 20, 8]
, k = 3
, candidates = 2
Step 1: Check edge case
candidates * 2 = 4
andn = 7
- Since
4 < 7
, we proceed with the two-window approach
Step 2: Initialize the heap
- Add first 2 workers:
(17, 0)
and(12, 1)
- Add last 2 workers:
(20, 5)
and(8, 6)
- Heap after initialization:
[(8, 6), (12, 1), (17, 0), (20, 5)]
Step 3: Set pointers
l = 2
(next left worker to consider)r = 4
(next right worker to consider)
Hiring Session 1:
- Extract minimum:
(8, 6)
→ cost = 8, index = 6 - Running total: 8
- Since
i = 6
is not less thanl = 2
, this worker came from the right window - Add worker at index
r = 4
:(7, 4)
to heap - Update
r = 3
- Heap now:
[(7, 4), (12, 1), (17, 0), (20, 5)]
Hiring Session 2:
- Extract minimum:
(7, 4)
→ cost = 7, index = 4 - Running total: 8 + 7 = 15
- Since
i = 4
is not less thanl = 2
, this worker came from the right window - Add worker at index
r = 3
:(2, 3)
to heap - Update
r = 2
- Heap now:
[(2, 3), (12, 1), (17, 0), (20, 5)]
Hiring Session 3:
- Extract minimum:
(2, 3)
→ cost = 2, index = 3 - Running total: 15 + 2 = 17
- Since
i = 3
is not less thanl = 2
, this worker came from the right window - Add worker at index
r = 2
:(10, 2)
to heap - Update
r = 1
- Now
l = 2
andr = 1
, sol > r
- the windows have met
Final Result: Total cost = 17
The algorithm efficiently selected the cheapest workers available from either end of the array at each step, maintaining the constraint of only considering candidates
workers from each end.
Solution Implementation
1class Solution:
2 def totalCost(self, costs: List[int], k: int, candidates: int) -> int:
3 """
4 Calculate minimum total cost to hire k workers.
5 Workers can be selected from 'candidates' number of workers from either end.
6
7 Args:
8 costs: List of hiring costs for each worker
9 k: Number of workers to hire
10 candidates: Number of workers to consider from each end
11
12 Returns:
13 Minimum total cost to hire k workers
14 """
15 num_workers = len(costs)
16
17 # If candidate pools from both ends overlap or cover all workers,
18 # simply take the k cheapest workers
19 if candidates * 2 >= num_workers:
20 return sum(sorted(costs)[:k])
21
22 # Min heap to store (cost, index) pairs
23 min_heap = []
24
25 # Add first 'candidates' workers from the left side
26 for index, cost in enumerate(costs[:candidates]):
27 heappush(min_heap, (cost, index))
28
29 # Add last 'candidates' workers from the right side
30 for index in range(num_workers - candidates, num_workers):
31 heappush(min_heap, (costs[index], index))
32
33 # Left and right pointers for the next workers to potentially add
34 left_pointer = candidates
35 right_pointer = num_workers - candidates - 1
36
37 total_cost = 0
38
39 # Hire k workers
40 for _ in range(k):
41 # Get the worker with minimum cost
42 worker_cost, worker_index = heappop(min_heap)
43 total_cost += worker_cost
44
45 # If no more workers to add (pools have met), continue
46 if left_pointer > right_pointer:
47 continue
48
49 # Add a new worker to the heap based on which side was selected
50 if worker_index < left_pointer:
51 # Worker was from left pool, add next worker from left
52 heappush(min_heap, (costs[left_pointer], left_pointer))
53 left_pointer += 1
54 else:
55 # Worker was from right pool, add next worker from right
56 heappush(min_heap, (costs[right_pointer], right_pointer))
57 right_pointer -= 1
58
59 return total_cost
60
1class Solution {
2 public long totalCost(int[] costs, int k, int candidates) {
3 int totalWorkers = costs.length;
4 long totalHiringCost = 0;
5
6 // Special case: if the candidate pools from both ends cover the entire array
7 // Simply sort and take the k cheapest workers
8 if (candidates * 2 >= totalWorkers) {
9 Arrays.sort(costs);
10 for (int i = 0; i < k; i++) {
11 totalHiringCost += costs[i];
12 }
13 return totalHiringCost;
14 }
15
16 // Min heap to track available candidates
17 // Each element is [cost, index] where we sort by cost first, then by index
18 PriorityQueue<int[]> candidateHeap = new PriorityQueue<>((a, b) -> {
19 if (a[0] == b[0]) {
20 return a[1] - b[1]; // If costs are equal, prefer lower index
21 }
22 return a[0] - b[0]; // Otherwise sort by cost
23 });
24
25 // Add initial candidates from the front of the array
26 for (int i = 0; i < candidates; i++) {
27 candidateHeap.offer(new int[] {costs[i], i});
28 }
29
30 // Add initial candidates from the back of the array
31 for (int i = 0; i < candidates; i++) {
32 candidateHeap.offer(new int[] {costs[totalWorkers - i - 1], totalWorkers - i - 1});
33 }
34
35 // Pointers to track the next available workers from front and back
36 int leftPointer = candidates;
37 int rightPointer = totalWorkers - candidates - 1;
38
39 // Hire k workers
40 while (k > 0) {
41 // Get the cheapest available candidate
42 int[] cheapestCandidate = candidateHeap.poll();
43 totalHiringCost += cheapestCandidate[0];
44
45 // If all workers have been considered, continue to next iteration
46 if (leftPointer > rightPointer) {
47 k--;
48 continue;
49 }
50
51 // Add a new candidate to replace the hired worker
52 // If hired worker was from the left pool, add next from left
53 if (cheapestCandidate[1] < leftPointer) {
54 candidateHeap.offer(new int[] {costs[leftPointer], leftPointer});
55 leftPointer++;
56 }
57 // If hired worker was from the right pool, add next from right
58 else {
59 candidateHeap.offer(new int[] {costs[rightPointer], rightPointer});
60 rightPointer--;
61 }
62
63 k--;
64 }
65
66 return totalHiringCost;
67 }
68}
69
1class Solution {
2public:
3 long long totalCost(vector<int>& costs, int k, int candidates) {
4 int totalWorkers = costs.size();
5
6 // Special case: if the candidate pools from both ends overlap or cover all workers
7 // Simply sort and pick the k cheapest workers
8 if (candidates * 2 >= totalWorkers) {
9 sort(costs.begin(), costs.end());
10 return accumulate(costs.begin(), costs.begin() + k, 0LL);
11 }
12
13 // Min heap to store (cost, index) pairs, automatically sorted by cost
14 using CostIndexPair = pair<int, int>;
15 priority_queue<CostIndexPair, vector<CostIndexPair>, greater<CostIndexPair>> minHeap;
16
17 // Initialize heap with candidates from both ends
18 for (int i = 0; i < candidates; ++i) {
19 minHeap.emplace(costs[i], i); // Add from left side
20 minHeap.emplace(costs[totalWorkers - i - 1], totalWorkers - i - 1); // Add from right side
21 }
22
23 long long totalCostSum = 0;
24 int leftBoundary = candidates; // Next available index from left
25 int rightBoundary = totalWorkers - candidates - 1; // Next available index from right
26
27 // Hire k workers
28 while (k--) {
29 // Get the worker with minimum cost
30 auto [workerCost, workerIndex] = minHeap.top();
31 minHeap.pop();
32 totalCostSum += workerCost;
33
34 // If all remaining workers are already in the heap, continue
35 if (leftBoundary > rightBoundary) {
36 continue;
37 }
38
39 // Refill the heap based on which side the hired worker came from
40 if (workerIndex < leftBoundary) {
41 // Worker was from left pool, add next worker from left
42 minHeap.emplace(costs[leftBoundary], leftBoundary);
43 leftBoundary++;
44 } else {
45 // Worker was from right pool, add next worker from right
46 minHeap.emplace(costs[rightBoundary], rightBoundary);
47 rightBoundary--;
48 }
49 }
50
51 return totalCostSum;
52 }
53};
54
1/**
2 * Calculates the minimum total cost of hiring exactly k workers
3 * @param costs - Array of hiring costs for each worker
4 * @param k - Number of workers to hire
5 * @param candidates - Number of candidates to consider from each end
6 * @returns The minimum total cost to hire k workers
7 */
8function totalCost(costs: number[], k: number, candidates: number): number {
9 const n = costs.length;
10
11 // If we can consider all workers as candidates, simply sort and take the k cheapest
12 if (candidates * 2 >= n) {
13 costs.sort((a, b) => a - b);
14 return costs.slice(0, k).reduce((sum, cost) => sum + cost, 0);
15 }
16
17 // Priority queue to maintain candidates with their costs and indices
18 // Sorted by cost first, then by index if costs are equal
19 const priorityQueue = new PriorityQueue({
20 compare: (a, b) => (a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]),
21 });
22
23 // Add initial candidates from both ends of the array
24 for (let i = 0; i < candidates; ++i) {
25 priorityQueue.enqueue([costs[i], i]); // Add from left side
26 priorityQueue.enqueue([costs[n - i - 1], n - i - 1]); // Add from right side
27 }
28
29 // Pointers for the next elements to consider from left and right
30 let leftPointer = candidates;
31 let rightPointer = n - candidates - 1;
32 let totalCost = 0;
33
34 // Hire k workers
35 while (k--) {
36 // Get the worker with minimum cost (and smallest index if tied)
37 const [workerCost, workerIndex] = priorityQueue.dequeue()!;
38 totalCost += workerCost;
39
40 // If all workers have been considered, continue
41 if (leftPointer > rightPointer) {
42 continue;
43 }
44
45 // Replace the hired worker with a new candidate
46 if (workerIndex < leftPointer) {
47 // Worker was from the left group, add next from left
48 priorityQueue.enqueue([costs[leftPointer], leftPointer++]);
49 } else {
50 // Worker was from the right group, add next from right
51 priorityQueue.enqueue([costs[rightPointer], rightPointer--]);
52 }
53 }
54
55 return totalCost;
56}
57
Time and Space Complexity
The time complexity is O((candidates + k) × log(candidates))
in the general case, but can be simplified to O(n × log n)
in the worst case scenario.
Let me break down the analysis:
-
Initial setup:
- When
candidates * 2 >= n
, we sort the entire array and return the sum of firstk
elements, which takesO(n × log n)
time. - Otherwise, we push
2 × candidates
elements into the heap, each takingO(log(candidates))
time, resulting inO(candidates × log(candidates))
time.
- When
-
Main loop:
- The loop runs
k
times - Each iteration performs one
heappop
operation:O(log(size of heap))
- Each iteration may perform one
heappush
operation:O(log(size of heap))
- The heap size is at most
2 × candidates
throughout the execution - Total time for the loop:
O(k × log(candidates))
- The loop runs
-
Overall time complexity:
- In the worst case where
candidates = n/2
andk = n
, the complexity becomesO(n × log n)
- In the general case:
O(candidates × log(candidates) + k × log(candidates))
=O((candidates + k) × log(candidates))
- In the worst case where
The space complexity is O(candidates)
for the heap storage, which in the worst case (when candidates = n/2
) becomes O(n)
.
Therefore, the reference answer correctly identifies the worst-case complexities as O(n × log n)
for time and O(n)
for space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Handling the Overlap Condition
Pitfall: A common mistake is not properly identifying when the candidate pools from both ends overlap. Some implementations use candidates >= n - candidates
or forget to multiply by 2, leading to incorrect edge case handling.
Why it happens: The overlap occurs when the sum of candidates from both ends meets or exceeds the total number of workers. The condition candidates * 2 >= n
ensures we catch all cases where the two windows would overlap or touch.
Solution: Always use candidates * 2 >= n
to check for overlap. This correctly identifies when:
- The windows actually overlap (e.g., n=5, candidates=3)
- The windows exactly meet (e.g., n=6, candidates=3)
- The windows more than cover the array (e.g., n=4, candidates=3)
2. Adding Duplicate Workers to the Heap
Pitfall: When initializing the heap with workers from both ends, failing to account for the overlap case can result in adding the same worker twice.
Example scenario:
# Wrong approach - adds duplicates when windows overlap
for i in range(candidates):
heappush(pq, (costs[i], i))
for i in range(n - candidates, n): # This might include indices already added!
heappush(pq, (costs[i], i))
Solution: Always check for the overlap condition first with if candidates * 2 >= n
before initializing the heap. This prevents duplicate entries entirely.
3. Incorrect Pointer Management
Pitfall: Setting up or updating the left and right pointers incorrectly, especially the initial values.
Common mistakes:
- Setting
r = n - candidates
instead ofr = n - candidates - 1
- Not checking
if l > r
before adding new workers to the heap - Incrementing/decrementing pointers before adding to heap instead of after
Solution: Initialize pointers as l = candidates
and r = n - candidates - 1
. These represent the next available workers to add from each side. Always verify l <= r
before attempting to add new workers.
4. Wrong Condition for Determining Which Side to Refill From
Pitfall: Using the wrong comparison to determine whether the hired worker came from the left or right pool.
Incorrect approach:
# Wrong - comparing with candidates instead of left_pointer if worker_index < candidates: # This doesn't account for pointer movement! # Add from left
Solution: Compare the hired worker's index with the current left_pointer
position: if worker_index < left_pointer
. This correctly identifies which pool the worker came from, even as the pointers move inward.
5. Not Handling the Case When Pools Meet
Pitfall: Continuing to add workers to the heap after the left and right pointers have crossed, potentially causing index out of bounds errors or incorrect results.
Solution: Always check if left_pointer > right_pointer
after hiring each worker. If true, skip adding new workers to the heap using continue
. The remaining workers in the heap are sufficient to complete the hiring process.
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
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
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!