用时最短的任务调度策略

79 阅读4分钟

用时最短的任务调度策略

问题背景

在一个分布式任务调度系统中,存在 taskNum 个需要被调度的任务。这些任务的编号从 1 到 taskNum。我们需要根据特定的调度策略,计算出完成所有任务所需的最短时间。

调度策略

  1. 依赖关系:任务之间可能存在依赖关系。这种关系是单向的,不存在循环依赖。例如,如果任务A依赖于任务B,那么任务A必须在任务B执行完毕后才能开始执行。
  2. 并发执行:如果多个任务之间没有任何直接或间接的依赖关系,它们就可以并行执行。我们假设系统资源是无限充足的,可以支持任意数量的任务并发执行。
  3. 任务耗时:每个任务的执行时间固定为 1 分钟。

目标

给定任务总数 taskNum 和一个描述任务间依赖关系的列表 relations,请计算出执行完所有 taskNum 个任务所需的最短时间(以分钟为单位)。


输入格式

  • taskNum: 一个整数,表示任务的总数量。

    • 1 <= taskNum <= 1000
  • relations: 一个二维数组(或列表的列表),表示任务间的依赖关系。

    • relations 中的每个元素 [id1, id2] 表示一个依赖关系,即 任务 id1 依赖于任务 id2
    • 0 <= relations.length <= 500000
    • 1 <= id1, id2 <= taskNum

输出格式

  • 一个整数,代表执行完所有任务所需的最短时间。

样例说明

样例输入

taskNum = 3
relations = [[1, 2]]

样例输出

2

解释

  1. 任务分析:

    • 总共有 3 个任务:任务1、任务2、任务3。
    • 依赖关系为 [1, 2],意味着任务1的执行依赖于任务2的完成。
    • 任务3与任务1、任务2之间没有依赖关系。
  2. 调度过程:

    • 第1分钟: 任务2和任务3没有前置依赖,因此它们可以并发执行。
    • 第2分钟: 第1分钟结束后,任务2完成。由于任务1依赖于任务2,此时任务1可以开始执行。
    • 任务3已在第1分钟完成,任务2也已完成。
  3. 结论:

    整个过程耗时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;
    }
}