📋 你可以安排的最多任务数目
📝 题目描述
给你n
个任务和m
个工人。每个任务需要一定的力量值才能完成,需要的力量值保存在下标从0开始的整数数组tasks
中,第i
个任务需要tasks[i]
的力量才能完成。每个工人的力量值保存在下标从0开始的整数数组workers
中,第j
个工人的力量值为workers[j]
。每个工人只能完成一个任务,且力量值需要大于等于该任务的力量要求值(即workers[j] >= tasks[i]
)。
除此之外,你还有pills
个神奇药丸,可以给一个工人的力量值增加strength
。你可以决定给哪些工人使用药丸,但每个工人最多只能使用一片药丸。
给你下标从0开始的整数数组tasks
和workers
以及两个整数pills
和strength
,请你返回最多有多少个任务可以被完成。
📋 代码模板
class Solution {
public int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
}
}
function maxTaskAssign(tasks: number[], workers: number[], pills: number, strength: number): number {
}
💡 提示
🚀 示例
🖊️ 题解
二分查找
假设工人们能够完成
二分查找的下限可以设置为
那么我们如何来验证工人们是否能够完成
为了判断力量值最大的
- 有工人的力量值大于等于该任务的所需力量值,设他们为
,那么我们不能浪费药丸,即不使用药丸。同时,让 中力量值最大的工人来完成该任务,这是因为 中的任意一个工人都能够完成剩余的任一任务,无论挑选哪个工人来完成该任务,都不会影响后续任务的完成。 - 所有工人的力量值都小于该任务的所需力量值,那么我们必须挑选一名工人并使用药丸来完成该任务,前提为:其服用药丸后的力量值需大于等于该任务的所需力量值。假设满足这个前提的工人集合为
,我们需让 中自身(不服用药丸)力量值最小的工人来完成该任务,这是因为 中任意一个工人在服用药丸后,都能够完成剩余的任一任务,无论挑选哪个工人来完成该任务,都不会影响后续任务的完成。
综上所述,我们可以使用一个有序集合
- 如果有序集合
中最大的元素大于等于 ,那么将最大的元素从有序集合 中删除,并进入下一轮任务枚举。 - 如果有序集合
中最大的元素小于 ,那么此时若没有药丸剩余或者有序集合中不存在大于等于 的元素,则说明我们就无法完成所有任务,可直接停止枚举,返回 false
;相反,该任务能够被完成,删除有序集合中第一个大于等于 的元素(这个过程可使用二分查找)。
class Solution {
public int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
Arrays.sort(tasks);
Arrays.sort(workers);
int low = 1, high = Math.min(tasks.length, workers.length);
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (check(tasks, workers, pills, strength, mid)) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return high;
}
private boolean check(int[] tasks, int[] works, int pills
, int strength, int k) {
int m = works.length;
List<Integer> wks = new ArrayList<>();
for (int i = m - k; i < m; i++) {
wks.add(works[i]);
}
for (int i = k - 1; i >= 0; i--) {
int last = wks.size() - 1;
if (wks.get(last) >= tasks[i]) {
wks.remove(last);
continue;
}
if (pills <= 0) {
return false;
}
int index = binarySearch(wks, tasks[i] - strength);
if (index == wks.size()) {
return false;
}
pills--;
wks.remove(index);
}
return true;
}
private int binarySearch(List<Integer> nums, int target) {
int low = 0, high = nums.size() - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (nums.get(mid) >= target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return low;
}
}
function maxTaskAssign(tasks: number[], workers: number[], pills: number, strength: number): number {
tasks.sort((n1, n2) => n1 - n2);
workers.sort((n1, n2) => n1 - n2);
const binarySearch = (nums: number[], target: number): number => {
let low = 0, high = nums.length - 1;
while (low <= high) {
const mid = low + Math.floor((high - low) / 2);
if (nums[mid] >= target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return low;
}
const check = (k: number, pills: number): boolean => {
const m = workers.length;
const wks: number[] = [];
for (let i = m - k; i < m; i++) {
wks.push(workers[i]);
}
for (let i = k - 1; i >= 0; i--) {
const last = wks.length - 1;
if (wks[last] >= tasks[i]) {
wks.splice(last, 1);
continue;
}
if (pills <= 0) {
return false;
}
const index = binarySearch(wks, tasks[i] - strength);
if (index == wks.length) {
return false;
}
pills--;
wks.splice(index, 1);
}
return true;
}
let low = 1, high = Math.min(tasks.length, workers.length);
while (low <= high) {
const mid = low + Math.floor((high - low) / 2);
if (check(mid, pills)) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return high;
}
二分查找 + 双端队列
在以上算法中,虽然删除有序集合
可以发现,对于当前任务的一次枚举中,如果存在多个大于等于
若双端队列中不存在元素,则说明,对于当前任务,即使在服用药丸后,也没有工人能够完成该任务,此时可提前结束枚举,并返回
false
。若最大元素大于等于
,不需要使用药丸,将其删除后进入下一轮枚举。 若最大元素小于
,则使用一颗药丸,并删除最小元素。需要注意的是,这个过程可能已经没有药丸可供使用,此时也应提前结束枚举,并返回 false
。
根据上述内容,可以得出:维护双端队列来替代有序集合,可以使删除工人元素的时间复杂度从
class Solution {
public int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
Arrays.sort(tasks);
Arrays.sort(workers);
int low = 1, high = Math.min(tasks.length, workers.length);
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (check(tasks, workers, pills, strength, mid)) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return high;
}
private boolean check(int[] tasks, int[] works, int pills
, int strength, int k) {
int m = works.length;
Deque<Integer> deque = new LinkedList<>();
int cursor = m - 1;
for (int i = k - 1; i >= 0; i--) {
while (cursor >= m - k && works[cursor] + strength >= tasks[i]) {
deque.addFirst(works[cursor]);
cursor--;
}
if (deque.isEmpty()) {
return false;
}
if (deque.getLast() >= tasks[i]) {
deque.removeLast();
continue;
}
if (pills <= 0) {
return false;
}
pills--;
deque.removeFirst();
}
return true;
}
}
function maxTaskAssign(tasks: number[], workers: number[], pills: number, strength: number): number {
tasks.sort((n1, n2) => n1 - n2);
workers.sort((n1, n2) => n1 - n2);
const check = (k: number, pills: number): boolean => {
const m = workers.length;
const deque: Deque<number> = new LinkedListDeque();
let cursor = m - 1;
for (let i = k - 1; i >= 0; i--) {
while (cursor >= m - k && workers[cursor] + strength >= tasks[i]) {
deque.pushFirst(workers[cursor]);
cursor--;
}
if (deque.isEmpty()) {
return false;
}
const last = deque.size() - 1;
if (deque.peekLast()! >= tasks[i]) {
deque.popLast();
continue;
}
if (pills <= 0) {
return false;
}
pills--;
deque.popFirst();
}
return true;
}
let low = 1, high = Math.min(tasks.length, workers.length);
while (low <= high) {
const mid = low + Math.floor((high - low) / 2);
if (check(mid, pills)) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return high;
}
interface Deque<T> {
pushLast(val: T): void;
pushFirst(val: T): void;
popLast(): T | null;
popFirst(): T | null;
peekLast(): T | null;
peekFirst(): T | null;
size(): number;
isEmpty(): boolean;
}
class LNode<T> {
prev: LNode<T> | null = null;
next: LNode<T> | null = null;
val: T;
constructor(val: T) {
this.val = val;
}
}
class LinkedListDeque<T> implements Deque<T> {
private front: LNode<T> | null = null;
private rear: LNode<T> | null = null;
private queueSize: number;
constructor() {
this.queueSize = 0;
}
pushLast(val: T): void {
const node = new LNode(val);
if (this.isEmpty()) {
this.front = this.rear = node;
} else {
this.rear!.next = node;
node.prev = this.rear;
this.rear = node;
}
this.queueSize++;
}
pushFirst(val: T): void {
const node = new LNode(val);
if (this.isEmpty()) {
this.front = this.rear = node;
} else {
this.front!.prev = node;
node.next = this.front;
this.front = node;
}
this.queueSize++;
}
popLast(): T | null {
if (this.isEmpty()) {
return null;
}
const value: T = this.rear!.val;
let temp: LNode<T> | null = this.rear!.prev;
if (temp) {
temp.next = null;
this.rear!.prev = null;
}
this.rear = temp;
this.queueSize--;
return value;
}
popFirst(): T | null {
if (this.isEmpty()) {
return null;
}
const value: T = this.front!.val;
let temp: LNode<T> | null = this.front!.next;
if (temp) {
temp.prev = null;
this.front!.next = null;
}
this.front = temp;
this.queueSize--;
return value;
}
peekLast(): T | null {
return this.isEmpty() ? null : this.rear!.val;
}
peekFirst(): T | null {
return this.isEmpty() ? null : this.front!.val;
}
size(): number {
return this.queueSize;
}
isEmpty(): boolean {
return this.queueSize == 0;
}
}
💭 复杂度分析
基于二分查找
的解决方案的复杂度分析如下。
- 时间复杂度:
,其中 , 为对数组 排序所需的时间复杂度, 为对数组 排序所需的时间复杂度。而 为二分查找答案的时间复杂度。 - 空间复杂度:
。其中 为对数组 排序所需要的栈的空间复杂度, 为对数组 排序所需要的栈的空间复杂度, 为二分查找所需要的有序集合的空间复杂度。
基于二分查找 + 双端队列
的解决方案的复杂度分析如下。
- 时间复杂度:
。 - 空间复杂度:
。