你有一台电脑,它可以 同时 运行无数个任务。给你一个二维整数数组 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开始向前排。
-
排序:首先,根据任务的结束时间对任务进行排序。这样我们可以保证在处理任务时,总是先处理最早需要结束的任务。
-
贪心策略:对于每个任务,我们尝试在可能的最晚时间开始,这样可以为其他任务腾出空间。
-
时间点标记:使用一个数组
run
来记录每个时间点是否已经被占用。初始化时,所有时间点都未被占用。 -
计算占用时间:遍历每个任务,检查在任务的
start
和end
时间之间,已经有多少时间点被占用。如果已经被占用的时间点总数小于任务的duration
,则需要额外占用时间点。 -
更新占用情况:对于需要额外占用的时间点,从
end
时间点向前检查,直到占用足够的时间点或到达start
时间点。 -
累计运行时间:累计所有任务实际占用的时间点数量,即为电脑最少需要运行的时间。
算法逻辑
- 按照任务的结束时间对任务列表进行排序。
- 初始化一个长度为
end
最大值加一的数组run
,所有元素初始值为0。 - 遍历排序后的任务列表:
- 对于每个任务,计算在它的
start
和end
时间范围内,已经被run
数组标记的时间点数量。 - 如果这个数量小于任务的
duration
,则需要占用额外的时间点。从end
开始向前找,直到占用了足够的时间点或到达start
。
- 对于每个任务,计算在它的
- 更新
run
数组,标记新占用的时间点。 - 累计所有任务占用的时间点数量,即为答案。
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 ]。
- 如果 end j − i + 1 = duration j ,那么说明当前任务 j 必须要在时间点 i 运行,标记当前时间 i;否则基于贪心的思想,当前任务 j 可以延后运行。
- 如果当前时间点 i 被标记为需要运行任务,那么我们遍历所有任务,将所有可以在当前时间点运行的任务都运行,同时更新对应任务的 duration。
最后返回所有运行的时间点数目。
这个解法采用了扫描线算法的思想,结合贪心策略来解决问题。扫描线算法是一种在几何问题中常用的算法,用于处理区间相关问题。在这里,我们将时间线视为一个区间,用扫描线逐一检查每个时间点。
算法逻辑
-
理解任务需求:每个任务都有一个开始时间
start
,结束时间end
,和所需时间duration
。任务可以在[start, end]
区间内任何时间开始,但必须完成duration
个时间点的工作。 -
初始化:设置一个变量
ans
来记录电脑总共需要运行的时间点数目,初始化为0。 -
确定扫描范围:找到所有任务中最晚的结束时间
m
,这个时间将作为我们扫描线算法的终点。 -
扫描线循环:从时间点1开始,逐一检查每个时间点
i
,直到m
。- 对于每个时间点
i
,我们检查是否需要在这个时间点运行任务。 - 遍历所有任务,如果任务的剩余
duration
正好等于end - i + 1
,说明这个任务必须在时间点i
开始执行,我们将标记run
为true
。 - 同时,我们检查所有任务是否都已完成,如果都已完成,则
finished
标记为true
,结束循环。
- 对于每个时间点
-
执行任务:如果时间点
i
被标记为需要运行任务,则:- 增加
ans
的计数,表示电脑在这个时间点需要运行。 - 更新所有任务的
duration
,对于所有在时间点i
内的任务,减少它们的duration
计数。
- 增加
-
移动到下一个时间点:将扫描线移动到下一个时间点
i + 1
。 -
返回结果:扫描完成后,返回
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 个时间需要新增时间点,我们从右到左依次将新增的时间点与栈的区间进行合并,然后将合并后的新区间压入栈中。
最后返回所有运行的时间。
算法逻辑
-
排序:首先,按照任务的结束时间对
tasks
数组进行排序。 -
初始化栈:使用一个栈来维护运行时间区间,初始时压入一个元素
{-1, -1, 0}
,表示在时间-1
到-1
之间没有运行时间。 -
二分查找:对于每个任务,使用二分查找在栈中找到其开始时间
start
所对应的位置。 -
计算已有运行时间:计算从上一个区间到当前任务开始时间
start
的运行时间总和total
。 -
贪心策略:
- 如果
total
大于等于当前任务的duration
,则当前任务可以在已有的运行时间内完成,不需要额外占用时间。 - 如果
total
小于当前任务的duration
,则计算需要额外占用的时间点数目。
- 如果
-
合并区间:如果当前任务结束时间
end
与栈顶元素的结束时间相差小于等于所需额外时间duration
,则合并区间,更新运行时间总和。 -
压入新区间:如果存在不能合并的情况,则将新的运行时间区间压入栈中。
-
返回结果:最后,栈顶元素的第三位即为所有任务总共需要的运行时间。
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)。