用时最短的任务调度策略
问题背景
在一个分布式任务调度系统中,存在 taskNum 个需要被调度的任务。这些任务的编号从 1 到 taskNum。我们需要根据特定的调度策略,计算出完成所有任务所需的最短时间。
调度策略
- 依赖关系:任务之间可能存在依赖关系。这种关系是单向的,不存在循环依赖。例如,如果任务A依赖于任务B,那么任务A必须在任务B执行完毕后才能开始执行。
- 并发执行:如果多个任务之间没有任何直接或间接的依赖关系,它们就可以并行执行。我们假设系统资源是无限充足的,可以支持任意数量的任务并发执行。
- 任务耗时:每个任务的执行时间固定为 1 分钟。
目标
给定任务总数 taskNum 和一个描述任务间依赖关系的列表 relations,请计算出执行完所有 taskNum 个任务所需的最短时间(以分钟为单位)。
输入格式
-
taskNum: 一个整数,表示任务的总数量。1 <= taskNum <= 1000
-
relations: 一个二维数组(或列表的列表),表示任务间的依赖关系。relations中的每个元素[id1, id2]表示一个依赖关系,即 任务id1依赖于任务id2。0 <= relations.length <= 5000001 <= id1, id2 <= taskNum
输出格式
- 一个整数,代表执行完所有任务所需的最短时间。
样例说明
样例输入
taskNum = 3
relations = [[1, 2]]
样例输出
2
解释
-
任务分析:
- 总共有 3 个任务:任务1、任务2、任务3。
- 依赖关系为
[1, 2],意味着任务1的执行依赖于任务2的完成。 - 任务3与任务1、任务2之间没有依赖关系。
-
调度过程:
- 第1分钟: 任务2和任务3没有前置依赖,因此它们可以并发执行。
- 第2分钟: 第1分钟结束后,任务2完成。由于任务1依赖于任务2,此时任务1可以开始执行。
- 任务3已在第1分钟完成,任务2也已完成。
-
结论:
整个过程耗时2分钟。因此,执行完所有任务的最短时间为2分钟。
import java.util.*;
/**
* 解决任务调度问题的实现类.
* 核心是使用拓扑排序(基于广度优先搜索的卡恩算法)来计算任务依赖图的“高度”,
* 这个高度即为完成所有任务所需的最短时间。
*/
public class TaskScheduler {
/**
* 主方法,计算执行完所有任务所需的最短时间。
*
* @param taskNum 任务总数
* @param relations 任务间的依赖关系数组
* @return 完成所有任务所需的最短时间(分钟)
*/
public int findMinTime(int taskNum, int[][] relations) {
// --- 步骤 1: 构建图的邻接表和入度表 ---
// 邻接表: Key是前置任务ID, Value是依赖于该前置任务的后续任务列表
Map<Integer, List<Integer>> adj = new HashMap<>();
// 入度表: Key是任务ID, Value是该任务的前置依赖数量
Map<Integer, Integer> inDegree = new HashMap<>();
// 初始化所有任务的邻接表和入度
for (int i = 1; i <= taskNum; i++) {
adj.put(i, new ArrayList<>());
inDegree.put(i, 0);
}
// 根据依赖关系填充邻接表和入度表
// 对于 [id1, id2],表示 id1 依赖 id2,即存在一条从 id2 到 id1 的有向边
for (int[] rel : relations) {
int prerequisite = rel[1]; // 前置任务
int dependent = rel[0]; // 后续任务
// 添加边: prerequisite -> dependent
adj.get(prerequisite).add(dependent);
// 后续任务的入度加一
inDegree.put(dependent, inDegree.get(dependent) + 1);
}
// --- 步骤 2: 找到所有初始可执行的任务 ---
// 使用队列来存储所有入度为0的节点,这些是第一批可以执行的任务
Queue<Integer> queue = new LinkedList<>();
for (int i = 1; i <= taskNum; i++) {
if (inDegree.get(i) == 0) {
queue.offer(i);
}
}
// --- 步骤 3: 逐层模拟并发执行过程 ---
int time = 0; // 记录经过的时间周期(分钟)
// 当队列不为空时,说明还有可以执行的任务
while (!queue.isEmpty()) {
// 开始一个新的时间周期
time++;
// 获取当前周期可以并发执行的任务数量
int levelSize = queue.size();
// 在这一个时间周期内,“执行”掉所有当前队列中的任务
for (int i = 0; i < levelSize; i++) {
int completedTask = queue.poll();
// 遍历所有依赖于这个已完成任务的后续任务
for (int dependentTask : adj.get(completedTask)) {
// 将其入度减1,表示一个前置依赖已完成
inDegree.put(dependentTask, inDegree.get(dependentTask) - 1);
// 如果这个后续任务的入度变为0,说明它的所有前置依赖都已满足
if (inDegree.get(dependentTask) == 0) {
// 那么它就可以在下一个时间周期被执行,将其加入队列
queue.offer(dependentTask);
}
}
}
}
// 循环结束时,time 的值就是图的层数,即所需的最短时间
return time;
}
}