LeetCode 2589. 完成所有任务的最少时间

2589. 完成所有任务的最少时间

你有一台电脑,它可以 同时 运行无数个任务。给你一个二维整数数组 tasks ,其中 tasks[i] = [starti, endi, durationi] 表示第 i 个任务需要在 闭区间 时间段 [starti, endi] 内运行 durationi 个整数时间点(但不需要连续)。

当电脑需要运行任务时,你可以打开电脑,如果空闲时,你可以将电脑关闭。

请你返回完成所有任务的情况下,电脑最少需要运行多少秒。

示例 1:

输入:tasks = [[2,3,1],[4,5,1],[1,5,2]]
输出:2
解释:
- 第一个任务在闭区间 [2, 2] 运行。
- 第二个任务在闭区间 [5, 5] 运行。
- 第三个任务在闭区间 [2, 2] 和 [5, 5] 运行。
电脑总共运行 2 个整数时间点。

示例 2:

输入:tasks = [[1,3,2],[2,5,3],[5,6,2]]
输出:4
解释:
- 第一个任务在闭区间 [2, 3] 运行
- 第二个任务在闭区间 [2, 3] 和 [5, 5] 运行。
- 第三个任务在闭区间 [5, 6] 运行。
电脑总共运行 4 个整数时间点。

提示:

  • 1 <= tasks.length <= 2000
  • tasks[i].length == 3
  • 1 <= starti, endi <= 2000
  • 1 <= durationi <= endi - starti + 1

提示 1

Sort the tasks in ascending order of end time


提示 2

Since there are only up to 2000 time points to consider, you can check them one by one


提示 3

It is always beneficial to run the task as late as possible so that later tasks can run simultaneously.

解法1: 贪心

题目分析

这个问题实际上是一个贪心算法的问题,我们需要找到完成所有任务的最少电脑运行时间。任务由三个参数定义:开始时间 start,结束时间 end,和所需时间 duration。关键在于理解,任务并不需要连续完成,只要在给定的时间段内完成足够的时间点即可。

解题思路

首先将 tasks 按照 end 从小到大进行排序。使用 run 数组标记哪些时间点有任务有运行,从小到大遍历数组 tasks,假设当前遍历的元素为 tasks[i]=[start i ​, end i , duration i ],统计 run 数组在时间段 [start i ​, end i ​] 内有运行的时间点数目 total: 

  • 如果 total ≥ duration i ​, 那么第 i 个任务可以放到先前运行的时间里运行。  
  • 如果 total < duration i ​, 那么我们可以将第 i 个任务的 total 个时间放到先前运行的时间里运行,duration i ​− total 个时间从右到左依次放到区间 [start i , end i ​] 内没有运行的时间点,从而保证后续的任务尽量利用先前任务的运行时间。  
  • 最后返回 run 里的总运行时间。  

采用贪心做法,其实可以把每个任务单独挑出来看。 每个任务的工作可以简单概括为:尽可能利用先前结束的工作,并为后面结束的工作创造并行条件。前半句是为什么要按结束顺序排序,后半句就是为什么要从ddl开始向前排。

  1. 排序:首先,根据任务的结束时间对任务进行排序。这样我们可以保证在处理任务时,总是先处理最早需要结束的任务。

  2. 贪心策略:对于每个任务,我们尝试在可能的最晚时间开始,这样可以为其他任务腾出空间。

  3. 时间点标记:使用一个数组 run 来记录每个时间点是否已经被占用。初始化时,所有时间点都未被占用。

  4. 计算占用时间:遍历每个任务,检查在任务的 startend 时间之间,已经有多少时间点被占用。如果已经被占用的时间点总数小于任务的 duration,则需要额外占用时间点。

  5. 更新占用情况:对于需要额外占用的时间点,从 end 时间点向前检查,直到占用足够的时间点或到达 start 时间点。

  6. 累计运行时间:累计所有任务实际占用的时间点数量,即为电脑最少需要运行的时间。

算法逻辑

  1. 按照任务的结束时间对任务列表进行排序。
  2. 初始化一个长度为 end 最大值加一的数组 run,所有元素初始值为0。
  3. 遍历排序后的任务列表:
    • 对于每个任务,计算在它的 start 和 end 时间范围内,已经被 run 数组标记的时间点数量。
    • 如果这个数量小于任务的 duration,则需要占用额外的时间点。从 end 开始向前找,直到占用了足够的时间点或到达 start
  4. 更新 run 数组,标记新占用的时间点。
  5. 累计所有任务占用的时间点数量,即为答案。

Java版:

class Solution {
    public int findMinimumTime(int[][] tasks) {
        int n = tasks.length;
        Arrays.sort(tasks, (a, b) -> a[1] - b[1]);
        int[] run = new int[tasks[n - 1][1] + 1];
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int start = tasks[i][0];
            int end = tasks[i][1];
            int duration = tasks[i][2];
            for (int j = start; j <= end; j++) {
                duration -= run[j];
            }
            ans += Math.max(duration, 0);
            for (int j = end; j >= start && duration > 0; j--) {
                if (run[j] == 0) {
                    run[j] = 1;
                    duration--;
                }
            }
        }
        return ans;
    }
}

Python3版:

class Solution:
    def findMinimumTime(self, tasks: List[List[int]]) -> int:
        tasks.sort(key = lambda x: x[1])
        run = [0] * (tasks[-1][1] + 1)
        ans = 0
        for start, end, duration in tasks:
            duration -= sum(run[start : end + 1])
            if duration <= 0:
                continue
            ans += duration
            for j in range(end, start - 1, -1):
                if duration <= 0:
                    break
                if run[j] == 0:
                    run[j] = 1
                    duration -= 1
        return ans

复杂度分析

  • 时间复杂度:O(n×M),其中 n 是 tasks 的大小,M 是 tasks 的时间段右端点 end 的最大值。
  • 空间复杂度:O(M+logn)。对 tasks 数组进行排序需要 O(logn),run 数组需要 O(M)。

解法2:贪心 + 扫描线

令 M 是 tasks 的时间段右端点 end 的最大值,我们可以利用扫描线的思想,依次扫描区间 [1,M],令当前扫描的时间点为 i:

遍历 tasks 数组,令当前遍历的任务为 tasks[j]=[start j ​ ,end j ​ ,duration j ​ ]。

  1. 如果 end j ​− i + 1 = duration j ​,那么说明当前任务 j 必须要在时间点 i 运行,标记当前时间 i;否则基于贪心的思想,当前任务 j 可以延后运行。
  2. 如果当前时间点 i 被标记为需要运行任务,那么我们遍历所有任务,将所有可以在当前时间点运行的任务都运行,同时更新对应任务的 duration。

最后返回所有运行的时间点数目。

这个解法采用了扫描线算法的思想,结合贪心策略来解决问题。扫描线算法是一种在几何问题中常用的算法,用于处理区间相关问题。在这里,我们将时间线视为一个区间,用扫描线逐一检查每个时间点。

算法逻辑

  1. 理解任务需求:每个任务都有一个开始时间 start,结束时间 end,和所需时间 duration。任务可以在 [start, end] 区间内任何时间开始,但必须完成 duration 个时间点的工作。

  2. 初始化:设置一个变量 ans 来记录电脑总共需要运行的时间点数目,初始化为0。

  3. 确定扫描范围:找到所有任务中最晚的结束时间 m,这个时间将作为我们扫描线算法的终点。

  4. 扫描线循环:从时间点1开始,逐一检查每个时间点 i,直到 m

    • 对于每个时间点 i,我们检查是否需要在这个时间点运行任务。
    • 遍历所有任务,如果任务的剩余 duration 正好等于 end - i + 1,说明这个任务必须在时间点 i 开始执行,我们将标记 run 为 true
    • 同时,我们检查所有任务是否都已完成,如果都已完成,则 finished 标记为 true,结束循环。
  5. 执行任务:如果时间点 i 被标记为需要运行任务,则:

    • 增加 ans 的计数,表示电脑在这个时间点需要运行。
    • 更新所有任务的 duration,对于所有在时间点 i 内的任务,减少它们的 duration 计数。
  6. 移动到下一个时间点:将扫描线移动到下一个时间点 i + 1

  7. 返回结果:扫描完成后,返回 ans,它表示电脑最少需要运行的时间点数目。

Java版:

class Solution {
    public int findMinimumTime(int[][] tasks) {
        int ans = 0;
        int i = 0;
        while (i >= 0) {
            boolean run = false;
            boolean finished = true;
            for (int[] task : tasks) {
                if (task[2] > 0 && task[1] - i + 1 == task[2]) {
                    run = true;
                }
                if (task[2] > 0) {
                    finished = false;
                }
            }
            if (finished) {
                break;
            }
            if (run) {
                ans++;
                for (int[] task : tasks) {
                    if (i >= task[0] && i <= task[1] && task[2] > 0) {
                        task[2]--;
                    }
                }
            }
            i++;
        }
        return ans;
    }
}

Python3版:

class Solution:
    def findMinimumTime(self, tasks: List[List[int]]) -> int:
        ans = 0
        m = max(tasks, key = lambda task: task[1])[1]
        for i in range(1, m + 1):
            run = False 
            for _, end, duration in tasks:
                if duration > 0 and end - i + 1 == duration:
                    run = True 
            if run:
                ans += 1
                for task in tasks:
                    if i >= task[0] and i <= task[1] and task[2] > 0:
                        task[2] -= 1
        return ans

复杂度分析

  • 时间复杂度:O(n×M),其中 n 是 tasks 的大小,M 是 tasks 的时间段右端点 end 的最大值。
  • 空间复杂度:O(1)。原地修改数组。

解法3:贪心 + 二分查找 + 栈

同类题型:LeetCode LCP 32. 批量处理任务-CSDN博客

首先将 tasks 按照 end 从小到大进行排序。类似于方法一,我们可以用时间区间替代 run 数组来维护有运行任务的时间点。使用栈依次保存新增的运行时间区间与总运行时间长度,初始时栈元素为 {−1,−1,0},我们遍历 tasks 数组,记当前遍历的元素为 tasks[i]=[start i ​ ,end i ​ ,duration i ​ ],通过二分查找找到所有在区间 [start i ​ ,end i ​ ] 内有运行任务的时间点数目 total:

如果 total≥duration i ​ ,那么当前任务可以放到先前运行的时间内运行。

如果 total<duration i ​ ,那么剩余 duration i ​ −total 个时间需要新增时间点,我们从右到左依次将新增的时间点与栈的区间进行合并,然后将合并后的新区间压入栈中。

最后返回所有运行的时间。

算法逻辑

  1. 排序:首先,按照任务的结束时间对 tasks 数组进行排序。

  2. 初始化栈:使用一个栈来维护运行时间区间,初始时压入一个元素 {-1, -1, 0},表示在时间 -1-1 之间没有运行时间。

  3. 二分查找:对于每个任务,使用二分查找在栈中找到其开始时间 start 所对应的位置。

  4. 计算已有运行时间:计算从上一个区间到当前任务开始时间 start 的运行时间总和 total

  5. 贪心策略

    • 如果 total 大于等于当前任务的 duration,则当前任务可以在已有的运行时间内完成,不需要额外占用时间。
    • 如果 total 小于当前任务的 duration,则计算需要额外占用的时间点数目。
  6. 合并区间:如果当前任务结束时间 end 与栈顶元素的结束时间相差小于等于所需额外时间 duration,则合并区间,更新运行时间总和。

  7. 压入新区间:如果存在不能合并的情况,则将新的运行时间区间压入栈中。

  8. 返回结果:最后,栈顶元素的第三位即为所有任务总共需要的运行时间。

Java版:

class Solution {
    public int findMinimumTime(int[][] tasks) {
        Arrays.sort(tasks, (a, b) -> a[1] - b[1]);
        List<int[]> stack = new ArrayList<>();
        stack.add(new int[]{-1, -1, 0});
        for (int[] task : tasks) {
            int start = task[0];
            int end = task[1];
            int duration = task[2];
            // 二分查找找到 start 所在的区间位置
            int k = binarySearch(stack, start);

            // 计算当前任务开始前已有的运行时间
            duration -= stack.get(stack.size() - 1)[2] - stack.get(k)[2];
            if (start <= stack.get(k)[1]) {
                duration -= stack.get(k)[1] - start + 1;
            }

            // 如果已有运行时间大于等于当前任务所需时间,则跳过
            if (duration <= 0) {
                continue;
            }

            // 合并区间,如果存在重叠则更新 duration
            while (end - stack.get(stack.size() - 1)[1] <= duration) {
                duration += stack.get(stack.size() - 1)[1] - stack.get(stack.size() - 1)[0] + 1;
                stack.remove(stack.size() - 1);
            }

            // 压入新的运行时间区间,并更新运行时间总和
            stack.add(new int[]{end - duration + 1, end, stack.get(stack.size() - 1)[2] + duration});
        }
        return stack.get(stack.size() - 1)[2];
    }

    private int binarySearch(List<int[]> stack, int target) {
        int l = 0;
        int r = stack.size() - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (stack.get(mid)[0] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return r;
    }
}

Python3版:

bisect.bisect和bisect.bisect_right返回大于x的第一个下标(相当于C++中的upper_bound),bisect.bisect_left返回大于等于x的第一个下标(相当于C++中的lower_bound)。

class Solution:
    def findMinimumTime(self, tasks: List[List[int]]) -> int:
        tasks.sort(key = lambda x: x[1])
        stack = [[-1, -1, 0]]
        for start, end, duration in tasks:
            # 二分查找找到 start 所在的区间位置
            k = bisect_left(stack, start, key = lambda x: x[0])

            # 计算当前任务开始前已有的运行时间
            duration -= stack[-1][2] - stack[k - 1][2]
            if start <= stack[k - 1][1]:
                duration -= stack[k - 1][1] - start + 1
            
            if duration <= 0:
                continue
            
            # 合并区间,如果存在重叠则更新 duration
            while end - stack[-1][1] <= duration:
                duration += stack[-1][1] - stack[-1][0] + 1
                stack.pop()
            
            # 压入新的运行时间区间,并更新运行时间总和
            stack.append([end - duration + 1, end, stack[-1][2] + duration])
        return stack[-1][2]

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是 tasks 的大小。

  • 空间复杂度:O(n)。

  • 32
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值