完成所有任务的最少时间

标签: 贪心 数组 二分查找 排序

难度: Hard

你有一台电脑,它可以 同时 运行无数个任务。给你一个二维整数数组 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

Submission

运行时间: 54 ms

内存: 17.1 MB

class Solution:
    def findMinimumTime(self, tasks: List[List[int]]) -> int:
        stack = [(-1, -1, 0)]  # (运行时间起点, 运行时间终点, 截止到当前区间的总运行时间),确保各运行区间不交;添加一个哨兵任务
        for start, end, d in sorted(tasks, key=lambda x: x[1]):  # 根据右端点升序排列
            # 统计[start, end]中有多少时间已运行
            l, r, s = stack[bisect_left(stack, (start,)) - 1]  # 最后一个可能与[start, end]相交的区间
            d -= stack[-1][2] - s  # 减去stack[i]之后的运行时间
            d -= r - start + 1 if start <= r else 0  # 减去stack[i]中可能的运行时间
            # 当前任务已完成
            if d <= 0:
                continue
            # 不断用stack[-1][1]和end之间的空当填补剩余运行时间d
            while end - stack[-1][1] <= d:
                l, r, s = stack.pop()
                d += r - l + 1
            stack.append((end - d + 1, end, stack[-1][2] + d))
        return stack[-1][2]

Explain

此题解使用贪心算法和区间合并策略,首先按照任务的结束时间对所有任务进行排序,确保我们总是优先处理最早结束的任务。使用一个栈来维护不重叠的运行时间段,栈中的每个元素记录当前任务的开始和结束时间以及截止当前任务的总运行时间。对于每个任务,我们检查它与栈中现有运行时间段的重叠情况,并计算还需运行的时间。如果当前任务的需求时间未被满足,我们会利用最后一个任务的结束时间与当前任务的结束时间之间的空隙来填充。这种方法确保了在满足所有任务的前提下,电脑的运行时间尽可能少。

时间复杂度: O(n log n)

空间复杂度: O(n)

class Solution:
    def findMinimumTime(self, tasks: List[List[int]]) -> int:
        stack = [(-1, -1, 0)]  # 添加哨兵任务,确保处理时不会出现索引错误
        for start, end, d in sorted(tasks, key=lambda x: x[1]):  # 按任务结束时间排序
            l, r, s = stack[bisect_left(stack, (start,)) - 1]  # 找到最后一个可能与当前任务相交的已有任务
            d -= stack[-1][2] - s  # 减去与当前任务堆叠的已有运行时间
            d -= r - start + 1 if start <= r else 0  # 减去重叠部分的运行时间
            if d <= 0:  # 如果没有剩余需求,继续下一个任务
                continue
            while end - stack[-1][1] <= d:  # 使用空隙填补剩余需求
                l, r, s = stack.pop()
                d += r - l + 1
            stack.append((end - d + 1, end, stack[-1][2] + d))  # 更新栈,添加新的时间段
        return stack[-1][2]  # 返回总运行时间

Explore

在题解中,二分查找是通过Python的`bisect_left`函数来实现的。这个函数用于在一个有序列表中查找插入点,使得插入后列表仍保持有序。在这个场景中,`bisect_left(stack, (start,))`是用来找到第一个起始时间不小于当前任务的起始时间的已处理任务。这是基于假设`stack`已经按照每个任务的结束时间有序排列。查找的条件是任务的起始时间,目的是为了确定当前任务可能受到哪些已经处理过的任务的影响(即找到可能与当前任务重叠的最后一个任务)。

代码中`d -= stack[-1][2] - s`这行代码的作用是从当前任务需要的运行时间`d`中减去由于与前一个任务重叠而已经计算过的运行时间。这里`stack[-1][2]`表示栈顶元素(即最后处理的时间段)的累积运行时间,而`s`是与当前任务可能重叠的最后一个任务的累积运行时间。这样`stack[-1][2] - s`就给出了在当前任务的开始时间前已经累积的运行时间,需要从`d`中减去这部分重叠的运行时间。 `d -= r - start + 1 if start <= r else 0`这行代码的作用是处理当前任务与前一个任务的时间重叠的情况。如果当前任务的开始时间`start`小于或等于前一个任务的结束时间`r`,则当前任务的部分或全部时间可能已被前一个任务覆盖。`r - start + 1`计算的是从当前任务开始到前一个任务结束的时间段长度,这段时间不需要再次计算,因此从`d`中减去。如果`start`大于`r`,则没有重叠,不需要减去任何值。