Facebook Pixel

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:

  1. Run k hiring sessions - In each session, you'll hire exactly one worker.

  2. 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
  3. 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
  4. Special case - If fewer than candidates workers remain, consider all remaining workers for selection.

  5. One-time selection - Each worker can only be hired once.

Example walkthrough:

  • Given costs = [3,2,7,7,1,2] and candidates = 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
  • 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

The goal is to return the total cost of hiring exactly k workers following these rules.

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

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 to candidates-1)
  • The last candidates workers (indices n-candidates to n-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 index candidates)
  • r points to the next worker to potentially add from the right (starts at index n - 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 index l and increment l
  • Otherwise, it was from the right window, so we add the next right worker at index r and decrement r

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 Evaluator

Example 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 and n = 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 than l = 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 than l = 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 than l = 2, this worker came from the right window
  • Add worker at index r = 2: (10, 2) to heap
  • Update r = 1
  • Now l = 2 and r = 1, so l > 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:

  1. Initial setup:

    • When candidates * 2 >= n, we sort the entire array and return the sum of first k elements, which takes O(n × log n) time.
    • Otherwise, we push 2 × candidates elements into the heap, each taking O(log(candidates)) time, resulting in O(candidates × log(candidates)) time.
  2. 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))
  3. Overall time complexity:

    • In the worst case where candidates = n/2 and k = n, the complexity becomes O(n × log n)
    • In the general case: O(candidates × log(candidates) + k × log(candidates)) = O((candidates + k) × log(candidates))

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 of r = 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.

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

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

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

Load More