Total Cost to Hire K Workers

Medium Heaps

Problem

Given an array costs representing the cost of hiring each worker, an integer k representing the number of workers to hire, and an integer candidates representing the number of candidates to consider from each end of the array, return the minimum total cost to hire exactly k workers by always choosing the lowest cost worker from the candidate pools at the front and back.

  • 1 ≤ costs.length ≤ 10⁵
  • 1 ≤ costs[i] ≤ 10⁵
  • 1 ≤ k, candidates ≤ costs.length
  • candidates ≤ costs.length / 2

Example

Input: costs = [17,12,10,2,7,2,11,20,8], k = 3, candidates = 4
Output: 11

Initially, the candidate pools are the first 4 workers [17,12,10,2] and the last 4 workers [2,11,20,8]. The algorithm pushes these candidates into a min-heap with their costs and indices. The lowest cost worker is 2 at index 3 (front pool), so hire them and add 2 to total. Then, the front pool pointer moves forward to index 4, and the worker at index 4 (cost 7) is pushed into the heap. Next, the lowest cost is 2 at index 5 (back pool), hire and add 2 to total. The back pool pointer moves backward to index 7, and the worker at index 7 (cost 20) is pushed into the heap. Finally, the lowest cost is 7 at index 4, hire and add 7 to total. The total cost is 2 + 2 + 7 = 11.

Approach

Straightforward Solution

A naive approach would repeatedly scan the front and back candidate pools to find the minimum cost worker, resulting in O(k * candidates) time complexity, which is too slow for large inputs.

Core Observation

At each hiring session, only workers from the front and back candidate pools are eligible. Maintaining a min-heap of these visible candidates allows efficient selection while pointers track the next unseen workers on each side.

Path to Optimal

The key insight is to use a min-heap to maintain the current candidate pools from both ends. Initially, push the first candidates and last candidates workers into the heap. After hiring a worker from one side, move the corresponding pointer inward and push the next worker into the heap if the pointers have not crossed. This approach maintains the candidate pools dynamically and allows extracting the minimum cost worker in O(log n) time per hire.

Optimal Approach

Use a min-heap to store tuples of (cost, index) for the current candidate pools. Initialize pointers front_end and back_start to track the boundaries of the candidate pools. For each of the k hires, pop the minimum cost worker from the heap, add their cost to the total, and push the next worker from the same side into the heap if available. This guarantees the minimum total cost with O(k log n) time complexity.

Time

O(k log candidates)

Each of the k hiring steps involves a heap pop and potentially a heap push, each O(log candidates), because the heap only stores workers from the current candidate pools.

Space

O(candidates)

The heap stores up to 2 * candidates elements initially and can grow up to O(n) in the worst case as pointers move inward, but never more than n elements total.

Pattern Spotlight

Heap / Priority Queue (Dynamic Candidate Pool Management)

When repeatedly selecting the minimum element from dynamically changing subsets at both ends of a sequence, maintain a min-heap of candidates from each side and update it incrementally as pointers move inward to achieve efficient selection.

Solution

1import heapq
2
3class Solution:
4 def totalCost(self, costs, k, candidates):
5
6 n = len(costs)
7 heap = []
8
9 front_end = candidates - 1
10 back_start = max(candidates, n - candidates)
11
12 for i in range(candidates):
13 heapq.heappush(heap, (costs[i], i))
14
15 for i in range(back_start, n):
16 heapq.heappush(heap, (costs[i], i))
17
18 total = 0
19
20 for _ in range(k):
21
22 cost, i = heapq.heappop(heap)
23 total += cost
24
25 if i <= front_end:
26 front_end += 1
27 if front_end < back_start:
28 heapq.heappush(heap, (costs[front_end], front_end))
29
30 else:
31 back_start -= 1
32 if back_start > front_end:
33 heapq.heappush(heap, (costs[back_start], back_start))
34
35 return total

Step-by-Step Solution

1

Initialize Candidate Pools and Min-Heap with Front and Back Workers

6n = len(costs)
7heap = []
9front_end = candidates - 1
10back_start = max(candidates, n - candidates)
12for i in range(candidates):
13 heapq.heappush(heap, (costs[i], i))
15for i in range(back_start, n):
16 heapq.heappush(heap, (costs[i], i))

Objective

To set up pointers delimiting the candidate pools and populate the min-heap with initial candidates from both ends.

Key Insight

By defining front_end and back_start pointers, the algorithm tracks the boundaries of the candidate pools. Pushing the first candidates workers and the last candidates workers into the min-heap ensures that the heap always contains the current eligible candidates from both ends. This setup enables efficient minimum cost extraction without scanning the entire array.

Interview Quick-Check

Core Logic

The min-heap stores tuples of (cost, index) from both candidate pools, enabling O(log n) extraction of the lowest cost worker.

State & Boundaries

front_end and back_start pointers prevent overlapping candidate pools and track which workers have been pushed into the heap.

Common Pitfalls & Bugs

Incorrectly initializing back_start can cause overlapping candidate pools or missing candidates; using max(candidates, n - candidates) ensures correct separation.

2

Iteratively Hire Lowest Cost Worker and Update Candidate Pools

18total = 0
20for _ in range(k):
22 cost, i = heapq.heappop(heap)
23 total += cost
25 if i <= front_end:
26 front_end += 1
27 if front_end < back_start:
28 heapq.heappush(heap, (costs[front_end], front_end))
30 else:
31 back_start -= 1
32 if back_start > front_end:
33 heapq.heappush(heap, (costs[back_start], back_start))

Objective

To repeatedly select the lowest cost worker from the heap, add their cost to the total, and update the candidate pools by pushing the next eligible worker from the same side.

Key Insight

Each hiring step pops the minimum cost worker from the heap, ensuring the optimal choice. Depending on whether the hired worker was from the front or back pool, the corresponding pointer moves inward. If the pointers have not crossed, the next worker from that side is pushed into the heap, maintaining the candidate pools dynamically. This incremental update avoids scanning and keeps the heap size manageable.

Interview Quick-Check

Core Logic

After hiring a worker, the algorithm pushes the next worker from the same side into the heap if available, preserving the candidate pools.

State & Boundaries

Pointers front_end and back_start are updated carefully to avoid overlapping candidate pools and ensure no worker is pushed twice.

Common Pitfalls & Bugs

Failing to check pointer boundaries before pushing new candidates can cause duplicates or invalid candidates in the heap.

Complexity

Each heap operation is O(log n), and there are k hires, resulting in O(k log n) time complexity.

3

Return the Accumulated Total Cost After Hiring K Workers

35return total

Objective

To output the final minimum total cost after completing all k hiring steps.

Key Insight

After k iterations of selecting the lowest cost worker and updating candidate pools, the total variable holds the sum of the costs of the hired workers. Returning this value completes the solution.

Interview Quick-Check

Core Logic

Returning the accumulated total cost reflects the sum of the optimally chosen workers' costs.

Line Analysis

This solution has 5 Critical lines interviewers watch for.

Line 22 Critical
cost, i = heapq.heappop(heap)

Pop the worker with the lowest cost from the heap.

Extracting the minimum cost worker ensures the hiring decision is optimal at each step.

Line 13 Critical
heapq.heappush(heap, (costs[i], i))

Push the current front candidate worker's cost and index into the heap.

Storing both cost and index allows the algorithm to track which side the worker belongs to for future updates.

Line 16 Critical
heapq.heappush(heap, (costs[i], i))

Push the current back candidate worker's cost and index into the heap.

Including the index enables the algorithm to distinguish back pool workers and update pointers accordingly.

Line 28 Critical
heapq.heappush(heap, (costs[front_end], front_end))

Push the next front candidate worker into the heap.

Adding the new candidate maintains the heap's representation of the current front pool.

Line 33 Critical
heapq.heappush(heap, (costs[back_start], back_start))

Push the next back candidate worker into the heap.

Maintains the heap's representation of the current back candidate pool after hiring.

Line 7 Core
heap = []

Initialize an empty min-heap to store candidate workers.

The min-heap enables efficient retrieval of the lowest cost worker among the candidate pools.

Line 10 Core
back_start = max(candidates, n - candidates)

Set the back_start pointer to the first index of the back candidate pool.

Using max(candidates, n - candidates) ensures the back candidate pool starts correctly without overlapping the front pool.

Line 25 Core
if i <= front_end:

Check if the hired worker was from the front candidate pool.

Determining the side of the hired worker is necessary to update the correct pointer and candidate pool.

Line 26 Core
front_end += 1

Move the front_end pointer forward to include the next worker.

Advancing the pointer expands the front candidate pool to include new workers as previous ones are hired.

Line 27 Core
if front_end < back_start:

Check if the front_end pointer has not crossed the back_start pointer.

Ensures candidate pools do not overlap, maintaining valid candidate boundaries.

Line 31 Core
back_start -= 1

Move the back_start pointer backward to include the next worker.

Advancing the back pointer inward expands the back candidate pool dynamically.

Line 32 Core
if back_start > front_end:

Check if the back_start pointer has not crossed the front_end pointer.

Prevents candidate pools from overlapping and ensures valid candidate selection.

Line 9 Core
front_end = candidates - 1

Set the front_end pointer to the last index of the front candidate pool.

This pointer tracks the boundary of the front candidate pool and helps manage which workers have been pushed into the heap.

Line 23 Core
total += cost

Add the hired worker's cost to the total.

Accumulates the cost of the chosen worker, building the final total cost.

Line 30 Core
else:

Handle the case where the hired worker was from the back candidate pool.

This branch updates the back candidate pool pointers and heap after hiring from the back.

Line 35 Core
return total

Return the total cost after hiring k workers.

Outputs the final accumulated cost, which is the minimum total cost achievable under the problem constraints.

Line 12 Standard
for i in range(candidates):

Iterate over the first candidates workers to add them to the heap.

This loop initializes the front candidate pool in the heap, enabling immediate access to these workers.

Line 15 Standard
for i in range(back_start, n):

Iterate over the last candidates workers to add them to the heap.

This loop initializes the back candidate pool in the heap, complementing the front pool.

Line 20 Standard
for _ in range(k):

Repeat the hiring process k times.

Each iteration represents hiring one worker, ensuring exactly k workers are hired.

Line 18 Standard
total = 0

Initialize the total cost accumulator to zero.

This variable accumulates the sum of hired workers' costs, which will be returned as the final answer.

Line 6 Basic
n = len(costs)

Calculate the length of the costs array.

Knowing the array length is essential for setting candidate pool boundaries and pointer initialization.

Test Your Understanding

Why does the algorithm maintain two pointers and dynamically update the heap instead of pushing all workers at once?

Because only the first candidates and last candidates workers are eligible at each step, pushing all workers at once would violate the problem constraints and could select workers outside the candidate pools. The two pointers and dynamic heap updates ensure only valid candidates are considered, preserving correctness and efficiency.

Related Problems

Heaps pattern

Don't just read it. Drill it.

Reconstruct Total Cost to Hire K Workers from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Total Cost to Hire K Workers drill

or drill a free problem