给你 n
个任务和 m
个工人。每个任务需要一定的力量值才能完成,需要的力量值保存在下标从 0 开始的整数数组 tasks
中,第 i
个任务需要 tasks[i]
的力量才能完成。每个工人的力量值保存在下标从 0 开始的整数数组 workers
中,第 j
个工人的力量值为 workers[j]
。每个工人只能完成 一个 任务,且力量值需要 大于等于 该任务的力量要求值(即 workers[j] >= tasks[i]
)。
除此以外,你还有 pills
个神奇药丸,可以给 一个工人的力量值 增加 strength
。你可以决定给哪些工人使用药丸,但每个工人 最多 只能使用 一片 药丸。
给你下标从 0 开始的整数数组tasks
和 workers
以及两个整数 pills
和 strength
,请你返回 最多 有多少个任务可以被完成。
示例 1:
输入:tasks = [3,2,1], workers = [0,3,3], pills = 1, strength = 1 输出:3 解释: 我们可以按照如下方案安排药丸: - 给 0 号工人药丸。 - 0 号工人完成任务 2(0 + 1 >= 1) - 1 号工人完成任务 1(3 >= 2) - 2 号工人完成任务 0(3 >= 3)
示例 2:
输入:tasks = [5,4], workers = [0,0,0], pills = 1, strength = 5 输出:1 解释: 我们可以按照如下方案安排药丸: - 给 0 号工人药丸。 - 0 号工人完成任务 0(0 + 5 >= 5)
示例 3:
输入:tasks = [10,15,30], workers = [0,10,10,10,10], pills = 3, strength = 10 输出:2 解释: 我们可以按照如下方案安排药丸: - 给 0 号和 1 号工人药丸。 - 0 号工人完成任务 0(0 + 10 >= 10) - 1 号工人完成任务 1(10 + 10 >= 15)
示例 4:
输入:tasks = [5,9,8,5,9], workers = [1,6,4,2,6], pills = 1, strength = 5 输出:3 解释: 我们可以按照如下方案安排药丸: - 给 2 号工人药丸。 - 1 号工人完成任务 0(6 >= 5) - 2 号工人完成任务 2(4 + 5 >= 8) - 4 号工人完成任务 3(6 >= 5)
提示:
n == tasks.length
m == workers.length
1 <= n, m <= 5 * 10^4
0 <= pills <= m
0 <= tasks[i], workers[j], strength <= 10^9
提示 1
Is it possible to assign the first k smallest tasks to the workers?
提示 2
How can you efficiently try every k?
解法1:二分 + 单调队列
题目分析
我们有一组任务 tasks
和一组工人 workers
,每个任务需要一定的力量值来完成,每个工人有一个初始的力量值。此外,我们还有一些药丸,可以增加工人的力量值。目标是尽可能多地完成任务。
解题思路
-
排序:首先,将任务和工人按照力量值进行排序。这样我们可以确保从力量最小的任务开始分配给力量最小的工人。
-
二分查找:使用二分查找来确定最多可以完成的任务数。二分查找的原因是,我们不知道最多可以完成多少任务,但我们可以通过不断尝试来逼近这个数字。
-
贪心策略:对于每个工人,我们尝试使用贪心策略来决定是否给他药丸。如果工人的力量加足以完成当前未完成的任务中力量要求最小的任务,我们就给他完成任务。如果没有任务可以完成,我们就尝试给他药丸并完成力量要求最大的任务(因为这样可以为后续的工人留下更多的选择)。
-
单调队列:使用单调队列来维护当前未完成的任务。这样可以快速地找到当前力量要求最小的任务和最大的任务。
算法逻辑
-
初始化:设置左右指针
l
和r
,以及药丸数量p
。 -
二分循环:在
l
和r
之间循环,尝试完成m = (l + r) / 2
个任务。 -
任务匹配:对于每个工人,从后向前遍历,尝试匹配任务。
- 如果工人的力量足以完成当前队列头部的任务(最小力量任务),就从队列头部移除这个任务。
- 如果不能,且还有药丸,就尝试完成队列尾部的任务(最大力量任务),并减少药丸数量。
-
结果判断:如果所有尝试匹配的工人都找到了可以完成的任务,则扩大搜索区间(
l = m + 1
);否则缩小搜索区间(r = m - 1
)。 -
返回结果:循环结束后,返回
r
,即最多可以完成的任务数。
深入理解
-
为什么使用二分查找:二分查找可以帮助我们快速确定可以完成的任务数的上限。由于问题要求最多完成多少任务,而这个数量是未知的,二分查找提供了一种有效的方法来逼近最优解。
-
为什么使用贪心策略:贪心策略在每一步都做出局部最优的选择,即对于每个工人,尽可能地完成力量要求合适的任务,这样可以最大化任务的完成数量。
-
为什么使用单调队列:单调队列可以快速地访问和更新当前力量要求最小和最大的任务,这对于贪心策略的实现至关重要。在队列头部快速找到力量要求最小的任务,在队列尾部快速找到力量要求最大的任务。
Java版:
class Solution {
public int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
Arrays.sort(tasks);
Arrays.sort(workers);
int l = 0;
int r = Math.min(tasks.length, workers.length);
while (l <= r) {
int m = l + (r - l) / 2;
int p = pills;
Deque<Integer> queTask = new ArrayDeque<>();
int idx = 0;
boolean res = true;
for (int i = workers.length - m; i < workers.length; i++) {
while (idx < m && workers[i] + strength >= tasks[idx]) {
queTask.offer(tasks[idx]);
idx++;
}
if (queTask.isEmpty()) {
res = false;
break;
}
if (workers[i] >= queTask.peek()) {
queTask.poll();
} else {
if (p == 0) {
res = false;
break;
}
p--;
queTask.pollLast();
}
}
if (res) {
l = m + 1;
} else {
r = m - 1;
}
}
return r;
}
}
Python3版:
class Solution:
def maxTaskAssign(self, tasks: List[int], workers: List[int], pills: int, strength: int) -> int:
tasks.sort()
workers.sort()
l = 0
r = min(len(tasks), len(workers))
while l <= r:
m = l + (r - l) // 2
p = pills
idx = 0
res = True
queTask = deque()
for i in range(len(workers) - m, len(workers)):
while idx < m and workers[i] + strength >= tasks[idx]:
queTask.append(tasks[idx])
idx += 1
if not queTask:
res = False
break
if workers[i] >= queTask[0]:
queTask.popleft()
elif p > 0:
p -= 1
queTask.pop()
else:
res = False
break
if res:
l = m + 1
else:
r = m - 1
return r
复杂度分析
-
时间复杂度:O(nlogn),其中 n 是任务或工人的数量。排序的时间复杂度是 O(nlogn),二分查找的每次迭代中,遍历和队列操作的时间复杂度是 O(n)。
-
空间复杂度:O(n),用于存储排序后的任务和工人数组,以及单调队列。