问题描述
给定一个下标从 0 开始的整数数组 costs
,其中 costs[i]
是雇佣第 i
位工人的代价。还给定两个整数 k
和 candidates
,我们需要按照以下规则来雇佣恰好 k
位工人:
- 总共进行
k
轮雇佣,每一轮雇佣一位工人。 - 在每一轮雇佣中,从最前面的
candidates
人和最后面的candidates
人中选出代价最小的工人。如果有多个代价相同的工人,选择下标较小的工人。 - 每一位工人只能被雇佣一次。
我们的目标是以最小的总代价雇佣到 k
位工人。
题目链接:2462. 雇佣 K 位工人的总代价 - 力扣(LeetCode)
解题思路
该问题可以通过 优先队列(即堆)来高效地解决。利用优先队列,我们能够快速找到当前候选工人中代价最小的工人。核心思路如下:
- 使用两个优先队列 分别管理前
candidates
和后candidates
工人,以方便比较和选择代价最小的工人。 - 每一轮选择时,比较两个队列中代价最小的工人,选择代价更低的工人进行雇佣。
- 双指针扩展范围,每次从工人列表的前后两端向内缩进,保证总是有足够的候选工人供选择。
关键步骤与优化
-
双端优先队列:
通过两个最小堆分别管理前candidates
个工人和后candidates
个工人。这样可以保证我们能够在常数时间内找到代价最小的工人。 -
双指针:
我们使用两个指针left
和right
,分别从工人列表的两端向内移动,用于动态管理当前可以选择的工人范围。 -
选择与更新:
每轮选择时,比较两个堆的堆顶工人(代价最小的工人),然后将该工人移出堆,并从剩余的工人中补充新工人进入对应的堆中。
代码实现
我们通过以下步骤逐步编写代码:
- 初始化两个优先队列(小顶堆)和双指针。
- 在每轮中从两个队列中选出代价最小的工人。
- 更新代价总和,并将已选择的工人从候选人中移除,继续补充新工人进入队列。
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;
long long hireWorkers(vector<int>& costs, int k, int candidates) {
// 定义两个小顶堆,分别管理前面的 candidates 个工人和后面的 candidates 个工人
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> leftHeap, rightHeap;
long long cost = 0; // 用于存储最终的总代价
int left = 0, right = costs.size() - 1;
// 初始化前面部分的候选工人
while (left < candidates && left <= right) {
leftHeap.push({costs[left], left});
left++;
}
// 初始化后面部分的候选工人
while (right >= costs.size() - candidates && left <= right) {
rightHeap.push({costs[right], right});
right--;
}
// 每轮选择一个工人,进行 k 轮选择
while (k > 0) {
k--;
// 比较两个堆的堆顶,选择代价较小的工人
if (!leftHeap.empty() && (rightHeap.empty() || leftHeap.top().first <= rightHeap.top().first)) {
cost += leftHeap.top().first; // 累加最小工人的代价
int idx = leftHeap.top().second;
leftHeap.pop(); // 从堆中移除该工人
// 如果还有工人未被考虑,将新工人加入左堆
if (left <= right) {
leftHeap.push({costs[left], left});
left++;
}
} else {
cost += rightHeap.top().first; // 累加最小工人的代价
int idx = rightHeap.top().second;
rightHeap.pop(); // 从堆中移除该工人
// 如果还有工人未被考虑,将新工人加入右堆
if (left <= right) {
rightHeap.push({costs[right], right});
right--;
}
}
}
return cost; // 返回雇佣 k 位工人的总代价
}
int main() {
vector<int> costs1 = {17, 12, 10, 2, 7, 2, 11, 20, 8};
int k1 = 3;
int candidates1 = 4;
cout << "Total hiring cost: " << hireWorkers(costs1, k1, candidates1) << endl;
vector<int> costs2 = {1, 2, 4, 1};
int k2 = 3;
int candidates2 = 3;
cout << "Total hiring cost: " << hireWorkers(costs2, k2, candidates2) << endl;
return 0;
}
代码详细解释
-
堆和指针初始化:
leftHeap
和rightHeap
分别管理前candidates
和后candidates
个工人。left
和right
是双指针,分别用于记录左边和右边尚未加入堆的工人位置。
-
每轮工人选择:
k
控制循环,每次从两端候选堆中选择代价最小的工人。- 比较
leftHeap
和rightHeap
的堆顶,选择代价更低的一侧。 - 根据选择的堆,将该工人移出并从剩余候选工人中补充新工人进入堆中。
-
终止条件:
- 使用
k
控制循环,每次选择一位工人。 - 比较
leftHeap
和rightHeap
的堆顶(最小元素),选择代价最小的工人。 - 从对应的堆中删除该工人,并根据当前剩余的工人情况从剩余部分补充新的工人进入堆中。
- 注意:
- 如果左边堆的堆顶较小或者右边堆为空,选择左边的工人,并且从剩余部分添加一个新的工人进入左边堆。
- 如果右边堆较小或者左边堆为空,则选择右边的工人并从剩余部分添加一个新的工人进入右边堆。
- 使用
关键逻辑解析
在代码中,以下这段判断逻辑是关键所在:
if (!leftHeap.empty() && (rightHeap.empty() || leftHeap.top().first <= rightHeap.top().first)) {
这一行代码决定了每一轮应该选择哪个堆中的工人,并且需要处理多个边界情况。我们来详细解释其中的逻辑:
-
!leftHeap.empty():
- 首先确保左边的优先队列 leftHeap 不为空。这是为了防止从空堆中取元素。
-
rightHeap.empty():
- 如果 rightHeap 为空,意味着右边部分没有工人可供选择,因此必须从 leftHeap 中选择。
-
leftHeap.top().first <= rightHeap.top().first:
- 如果 rightHeap 不为空,则需要比较两个堆的堆顶工人代价,选择代价更小的那个。
- 使用 <= 是为了保证当两个堆顶的代价相等时,我们优先选择左边的工人,因为题目要求在代价相同的情况下选择下标较小的工人。
if 条件确保我们总是从两个部分中选择代价更小的工人,并且当代价相等时,优先选择下标更小的那一侧。通过这种方式,我们能够保证每一轮雇佣的工人都是代价最小且满足题目要求的。
else 条件的分析
如果不满足上述 if 条件,那么会执行 else 分支,即从 rightHeap 中选择工人。这种情况会发生在以下几种场景:
-
leftHeap 为空:
- 如果 leftHeap 已经空了,而 rightHeap 仍有可供选择的工人,那么我们只能从右边部分中选择。
-
rightHeap 的代价更小:
- 当 rightHeap 中的堆顶工人的代价比 leftHeap 中的更小时,我们自然选择代价更小的工人来满足代价最小化的需求。
通过这种逻辑,我们确保每一轮总是优先从两端候选人中选择代价最低的工人,并且当左边堆为空时合理地从右边选择。这种方法保证了问题中的所有条件得到了满足,并且最小化了雇佣的总代价。
示例分析
示例 1:
输入:costs = [17, 12, 10, 2, 7, 2, 11, 20, 8]
, k = 3
, candidates = 4
- 第一轮:
leftHeap
初始化为[17, 12, 10, 2]
,rightHeap
初始化为[7, 2, 11, 20]
。- 选择代价最小的工人:从
leftHeap
选择2
,总代价为2
。
- 第二轮:
leftHeap
更新为
[17, 12, 10, 7]
,rightHeap
更新为 [7, 11, 20]
。
- 选择代价最小的工人:从
rightHeap
选择2
,总代价为4
。 - 第三轮:
leftHeap
更新为[17, 12, 10]
,rightHeap
更新为[7, 11]
。- 选择代价最小的工人:从
rightHeap
选择7
,最终总代价为11
。
输出:11
总结
通过这个题目,我们学到了如何使用优先队列(小顶堆)来动态地管理双端队列中的候选人,并以此来确保每次选择的工人代价最低。我们使用双指针和两个优先队列分别管理前后部分,从而高效地实现了对 costs 数组的动态管理。
重点逻辑在于合理比较两个堆的堆顶,确保代价最小且符合题目的要求。希望通过这篇博客,你可以更好地理解双端优先队列的使用以及复杂条件判断的技巧。