雇佣 K 位工人的总代价 图解题解
这道题到底在问什么
- 输入
- costs=[17,12,10,2,7,2,11,20,8], k=3, candidates=4
- 输出
- 11
最优解:一步一步想明白
- 3记住「左右各取 candidates 个进最小堆 → 每轮弹堆顶最低 → 从弹出那侧补一个」,下面每一帧都在套它。
- 4开局候选池是空的。左指针在最左 0、右指针在最右 8。先从左边连取 4 个工人放进最小堆。
- 5从左边取下标 0 的工人(成本 17)放进候选池。左指针右移到 1。
- 6从左边取下标 1 的工人(成本 12)放进候选池。左指针右移到 2。
- 7从左边取下标 2 的工人(成本 10)放进候选池。左指针右移到 3。
- 8从左边取下标 3 的工人(成本 2)放进候选池。左指针右移到 4。
- 9从右边取下标 8 的工人(成本 8)放进候选池。右指针左移到 7。
- 10从右边取下标 7 的工人(成本 20)放进候选池。右指针左移到 6。
- 11从右边取下标 6 的工人(成本 11)放进候选池。右指针左移到 5。
- 12从右边取下标 5 的工人(成本 2)放进候选池。右指针左移到 4。
- 13候选池建好了,共 8 个候选;中间灰色那段还没进过池。每一轮就弹一次堆顶(成本最低、同成本下标小),开始雇人。
- 14第 1 轮雇人:候选池里成本最低的是下标 3 的工人(成本 2、和另一个成本 2 并列,按下标小的先选),雇下他(绿色),总代价加到 2。他来自左侧。接下来按规则从左侧补一个新候选进池。
- 15从左侧补一个:下标 4 的工人(成本 7)进池,候选池又回到 8 个,留给下一轮挑。
- 16第 2 轮雇人:候选池里成本最低的是下标 5 的工人(成本 2),雇下他(绿色),总代价加到 4。他来自右侧。此时两端候选已取光(left=5 > right=4),不再补新人,后面直接从池里弹最低。
- 17这时 left=5 已经越过 right=4,两端候选取光、没有新人可补,候选池剩 7 个,下一轮直接从池里弹最低。
- 18第 3 轮雇人:候选池里成本最低的是下标 4 的工人(成本 7),雇下他(绿色),总代价加到 11。他来自左侧。这已是第 3 个、k 人凑满,算法到此停止、不再补人。
- 19第 3 个人也雇好了,k 人已凑满,代码到这就停止,不会再雇了。此时 left=5 也已经越过 right=4,本就没有新人可补。
- 20雇满 3 人,依次是成本 2、2、7,加起来最小总代价 11。靠「两端候选 + 最小堆 + 弹一个补一个」,每轮都全局取到当前能选的最低成本,不用反复扫整排。
⚠️ 容易写错的地方
✗ 错:左右候选选重叠了
✓ 对:每次取候选前判 left ≤ right
两端往中间靠,一旦 left > right 就没有新候选,补位也要停,否则会重复雇同一个人
✗ 错:成本相同时随便选
✓ 对:成本相同选下标更小的
题目规定平局取下标小者,堆要按「成本、再按下标」双关键字排序
✗ 错:补位从错误的一侧补
✓ 对:从「弹出者所在那一侧」补
雇掉的是左侧候选就从左补、右侧就从右补,才维持两端各 candidates 个的格局
完整代码(Python / C++ / Java)
Python
from typing import List
import heapq
class Solution:
def totalCost(self, costs: List[int], k: int, candidates: int) -> int:
n = len(costs)
heap = []
left, right = 0, n - 1
for _ in range(candidates):
if left <= right:
heapq.heappush(heap, (costs[left], left, 0))
left += 1
for _ in range(candidates):
if left <= right:
heapq.heappush(heap, (costs[right], right, 1))
right -= 1
ans = 0
for _ in range(k):
cost, idx, side = heapq.heappop(heap)
ans += cost
if left <= right:
if side == 0:
heapq.heappush(heap, (costs[left], left, 0))
left += 1
else:
heapq.heappush(heap, (costs[right], right, 1))
right -= 1
return ansC++
#include <queue>
#include <tuple>
#include <vector>
using namespace std;
class Solution {
public:
long long totalCost(vector<int>& costs, int k, int candidates) {
using T = tuple<int,int,int>;
priority_queue<T, vector<T>, greater<T>> pq;
int n = costs.size(), left = 0, right = n - 1;
for (int i = 0; i < candidates && left <= right; ++i) pq.push({costs[left], left++, 0});
for (int i = 0; i < candidates && left <= right; ++i) pq.push({costs[right], right--, 1});
long long ans = 0;
while (k--) {
auto [cost, idx, side] = pq.top(); pq.pop();
ans += cost;
if (left <= right) {
if (side == 0) pq.push({costs[left], left++, 0});
else pq.push({costs[right], right--, 1});
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public long totalCost(int[] costs, int k, int candidates) {
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
int left = 0, right = costs.length - 1;
for (int i = 0; i < candidates && left <= right; i++) pq.offer(new int[]{costs[left], left++, 0});
for (int i = 0; i < candidates && left <= right; i++) pq.offer(new int[]{costs[right], right--, 1});
long ans = 0;
while (k-- > 0) {
int[] cur = pq.poll();
ans += cur[0];
if (left <= right) {
if (cur[2] == 0) pq.offer(new int[]{costs[left], left++, 0});
else pq.offer(new int[]{costs[right], right--, 1});
}
}
return ans;
}
}复杂度
时间
O((candidates + k) log candidates)
堆里最多 2·candidates 个元素;初始化逐个入堆是 O(candidates log candidates),之后每轮一次弹出加一次插入各 O(log candidates)、共 k 轮
空间
O(candidates)
最小堆最多装两端各 candidates 个候选
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 雇佣 K 位工人的总代价 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
左右指针相遇(left > right)后还怎么继续雇人?+
相遇说明两端的候选已经全部进过堆了,后面不再补新候选,只是继续从堆里弹最低的,直到雇满 k 人。堆里此刻装的就是所有还没被雇的人,弹 k 减去已雇的剩余次数即可。
为什么候选元素要把下标也存进堆?+
一是平局时要按下标小的优先,堆得用「成本、再按下标」排序;二是有些写法要靠下标判断它来自左侧还是右侧、好决定从哪侧补位。直接只存成本会丢掉这些信息。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 雇佣 K 位工人的总代价 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。