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.
这个问题实际上是一个贪心算法的问题,我们需要找到完成所有任务的最少电脑运行时间。任务由三个参数定义:开始时间 start
,结束时间 end
,和所需时间 duration
。关键在于理解,任务并不需要连续完成,只要在给定的时间段内完成足够的时间点即可。
首先将 tasks 按照 end 从小到大进行排序。使用 run 数组标记哪些时间点有任务有运行,从小到大遍历数组 tasks,假设当前遍历的元素为 tasks[i]=[start i , end i , duration i ],统计 run 数组在时间段 [start i , end i ] 内有运行的时间点数目 total:
采用贪心做法,其实可以把每个任务单独挑出来看。 每个任务的工作可以简单概括为:尽可能利用先前结束的工作,并为后面结束的工作创造并行条件。前半句是为什么要按结束顺序排序,后半句就是为什么要从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
复杂度分析
令 M 是 tasks 的时间段右端点 end 的最大值,我们可以利用扫描线的思想,依次扫描区间 [1,M],令当前扫描的时间点为 i:
遍历 tasks 数组,令当前遍历的任务为 tasks[j]=[start j ,end j ,duration j ]。
最后返回所有运行的时间点数目。
这个解法采用了扫描线算法的思想,结合贪心策略来解决问题。扫描线算法是一种在几何问题中常用的算法,用于处理区间相关问题。在这里,我们将时间线视为一个区间,用扫描线逐一检查每个时间点。
理解任务需求:每个任务都有一个开始时间 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
复杂度分析
同类题型: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
最后返回所有运行的时间。
排序:首先,按照任务的结束时间对 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 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 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)。