Hard

题目描述

给你 n 个任务和 m 个工人。每个任务需要一定的力量值才能胜任,第 i 个任务需要 tasks[i] 的力量才能胜任。每个工人的力量值为 workers[j] ,表示这个工人的力量值。每个工人只能被安排到一个任务上,但是一个任务可以无人安排。

除此以外,你还有 pills 个神奇药丸,可以给工人的力量值 增加 strength 。你可以决定给哪些工人使用药丸,但每个工人 最多 只能使用 一个 药丸。

给你下标从 0 开始的整数数组 tasksworkers 以及两个整数 pillsstrength ,请你返回 最多 有多少个任务可以被完成。

示例 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)
最后一个药丸没有给出,因为它不会使任何工人强大到足以完成最后一个任务。

提示:

  • n == tasks.length
  • m == workers.length
  • 1 <= n, m <= 5 * 10^4
  • 0 <= pills <= m
  • 0 <= tasks[i], workers[j], strength <= 10^9

解题思路

这是一道典型的二分答案 + 贪心验证的问题。

核心思路:

  1. 二分答案:能完成的任务数具有单调性,如果能完成 k 个任务,那么一定能完成 k-1 个任务。因此可以二分枚举答案。

  2. 贪心验证:对于固定的任务数 k,需要验证是否可行。关键洞察是应该优先完成最简单的 k 个任务,这样成功概率最大。

  3. 任务分配策略

    • 将任务和工人都按力量值排序
    • 对于前 k 个最简单的任务,使用双端队列维护可用工人
    • 对每个任务,优先使用刚好能完成的工人(不使用药丸)
    • 如果没有合适工人,使用药丸让最弱的能胜任工人完成任务
  4. 具体算法

    • 对任务升序排序,对工人降序排序
    • 使用双端队列,从强到弱加入能完成当前任务的工人
    • 对每个任务:先尝试不用药丸的最弱工人,再尝试用药丸的最弱工人
    • 贪心原则:能不用药丸就不用,用药丸时选择最弱的工人

这种方法的时间复杂度为 O(n log n + m log m + k log k),其中 k 是二分的范围。

代码实现

class Solution {
public:
    int maxTaskAssign(vector<int>& tasks, vector<int>& workers, int pills, int strength) {
        sort(tasks.begin(), tasks.end());
        sort(workers.begin(), workers.end(), greater<int>());
        
        int left = 0, right = min((int)tasks.size(), (int)workers.size());
        
        while (left < right) {
            int mid = (left + right + 1) / 2;
            if (canComplete(tasks, workers, mid, pills, strength)) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        
        return left;
    }
    
private:
    bool canComplete(vector<int>& tasks, vector<int>& workers, int k, int pills, int strength) {
        deque<int> dq;
        int j = 0;
        int usedPills = 0;
        
        for (int i = 0; i < k; i++) {
            // 添加所有能完成当前任务的工人(可能需要药丸)
            while (j < workers.size() && workers[j] + strength >= tasks[i]) {
                dq.push_back(workers[j]);
                j++;
            }
            
            // 如果队列中有工人能不用药丸完成任务
            if (!dq.empty() && dq.back() >= tasks[i]) {
                dq.pop_back();  // 使用最弱的能胜任的工人
            } 
            // 否则需要使用药丸
            else if (!dq.empty() && usedPills < pills) {
                dq.pop_front();  // 使用最强的工人加药丸
                usedPills++;
            } 
            // 无法完成这个任务
            else {
                return false;
            }
        }
        
        return true;
    }
};
class Solution:
    def maxTaskAssign(self, tasks: List[int], workers: List[int], pills: int, strength: int) -> int:
        from collections import deque
        
        tasks.sort()
        workers.sort(reverse=True)
        
        def canComplete(k):
            dq = deque()
            j = 0
            used_pills = 0
            
            for i in range(k):
                # 添加所有能完成当前任务的工人(可能需要药丸)
                while j < len(workers) and workers[j] + strength >= tasks[i]:
                    dq.append(workers[j])
                    j += 1
                
                # 如果队列中有工人能不用药丸完成任务
                if dq and dq[-1] >= tasks[i]:
                    dq.pop()  # 使用最弱的能胜任的工人
                # 否则需要使用药丸
                elif dq and used_pills < pills:
                    dq.popleft()  # 使用最强的工人加药丸
                    used_pills += 1
                # 无法完成这个任务
                else:
                    return False
            
            return True
        
        left, right = 0, min(len(tasks), len(workers))
        
        while left < right:
            mid = (left + right + 1) // 2
            if canComplete(mid):
                left = mid
            else:
                right = mid - 1
        
        return left
public class Solution {
    public int MaxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
        Array.Sort(tasks);
        Array.Sort(workers, (a, b) => b.CompareTo(a));
        
        int left = 0, right = Math.Min(tasks.Length, workers.Length);
        
        while (left < right) {
            int mid = (left + right + 1) / 2;
            if (CanComplete(tasks, workers, mid, pills, strength)) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        
        return left;
    }
    
    private bool CanComplete(int[] tasks, int[] workers, int k, int pills, int strength) {
        var dq = new LinkedList<int>();
        int j = 0;
        int usedPills = 0;
        
        for (int i = 0; i < k; i++) {
            // 添加所有能完成当前任务的工人(可能需要药丸)
            while (j < workers.Length && workers[j] + strength >= tasks[i]) {
                dq.AddLast(workers[j]);
                j++;
            }
            
            // 如果队列中有工人能不用药丸完成任务
            if (dq.Count > 0 && dq.Last.Value >= tasks[i]) {
                dq.RemoveLast();  // 使用最弱的能胜任的工人
            } 
            // 否则需要使用药丸
            else if (dq.Count > 0 && usedPills < pills) {
                dq.RemoveFirst();  // 使用最强的工人加药丸
                usedPills++;
            } 
            // 无法完成这个任务
            else {
                return false;
            }
        }
        
        return true;
    }
}
var maxTaskAssign = function(tasks, workers, pills, strength) {
    tasks.sort((a, b) => a - b);
    workers.sort((a, b) => b - a);
    
    const canComplete = (k) => {
        const dq = [];
        let j = 0;
        let usedPills = 0;
        
        for (let i = 0; i < k; i++) {
            // 添加所有能完成当前任务的工人(可能需要药丸)
            while (j < workers.length && workers[j] + strength >= tasks[i]) {
                dq.push(workers[j]);
                j++;
            }
            
            // 如果队列中有工人能不用药丸完成任务
            if (dq.length > 0 && dq[dq.length - 1] >= tasks[i]) {
                dq.pop();  // 使用最弱的能胜任的工人
            } 
            // 否则需要使用药丸
            else if (dq.length > 0 && usedPills < pills) {
                dq.shift();  // 使用最强的工人加药丸
                usedPills++;
            } 
            // 无法完成这个任务
            else {
                return false;
            }
        }
        
        return true;
    };
    
    let left = 0, right = Math.min(tasks.length, workers.length);
    
    while (left < right) {
        const mid = Math.floor((left + right + 1) / 2);
        if (canComplete(mid)) {
            left = mid;
        } else {
            right = mid - 1;
        }
    }
    
    return left;
};

复杂度分析

复杂度类型分析
时间复杂度O((n + m) log(min(n, m)))
空间复杂度O(m)

时间复杂度分析:

  • 排序:O(n log n + m log m)
  • 二分答案:O(log(min(n, m))) 次
  • 每次验证:O(n + m)
  • 总时间复杂度:O((n + m) log(min(n, m)))

空间复杂度分析:

  • 双端队列最多存储 m 个工人:O(m)

相关题目