每日涮法记录--图拓扑排序(课程表)

140 阅读4分钟
本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

今天回顾了一下数据结构当中的图,于是上leetcode刷了一波相关题型。记录这道经典课程表题
题目: 现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。- 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1]  返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。 加上几个事例+提示 image.png

生活的例子

  • 先穿内裤再穿裤子,先穿打底再穿外套,先穿衣服再戴帽子,约定俗成
  • 内裤外穿、光着身子戴帽等,都会有点奇怪
  • 约束我们的一条条 先后规则,能否转成一串顺序行为——衣服是一件件穿的

引入有向图 描述依赖关系

  • 示例:n = 6,先决条件表:[ [3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4] ]
  • 0, 1, 2 没有先修课,可以直接选。其余的,都要先修 2 门课
  • 我们用 有向图 描述这种 依赖关系 (做事的先后关系): 微信截图_20200517052852.png
  • 把这样一个 有向无环图 变成 线性的排序 就叫 拓扑排序
  • 有向图 中有 入度出度 概念:
    • 如果存在一条有向边 A --> B,则这条边给 A 增加了 1 个出度,给 B 增加了 1 个入度
  • 所以顶点 0、1、2 的 入度为 0。 顶点 3、4、5 的 入度为 2

每次只能选你能上的课

  • 每次只能选 入度为 0 的课,因为它不依赖别的课
  • 假设选了 0,导致 依赖 0 的课的入度减小,课 3 的入度由 2 变 1
  • 接着选 1,导致课 3 的入度变 0,课 4 的入度由 2 变 1
  • 接着选 2,导致课 4 的入度变 0,当前 3 和 4 入度为 0
  • 继续选 入度为 0 的课 …… 直到选不到 入度为 0 的课

形似 树的BFS

  • 起初让 入度为 0 的课 入列
  • 然后逐个出列,课出列 即 课被选 ,并 减小相关课的入度
  • 判定是否有课的入度新变为 0,安排入列、再出列……
  • 直到没有 入度为 0 的课 可入列……

BFS 前的准备工作

  • 我们关心 课的入度 —— 该值要被减,要被监控
  • 我们关心 课之间的依赖关系 —— 选这门课会减小哪些课的入度
  • 因此我们需要合适的数据结构,去存储这些关系

入度数组 和 邻接表

  • 课号是 0 到 n - 1,作为索引,值为入度。遍历先决条件表,求出每门课的初始入度
  • 用哈希表记录 依赖关系 (也可以用 邻接矩阵 ,但它有点大)
    • key: 课的编号
    • value: 依赖它的后续课程

BFS 思路

  • queue 队列中始终是【入度为 0 的课】在里面流动
  • 选择一门课,就让它 出列,同时 查看哈希表,看它 对应哪些后续课
  • 将这些后续课的 入度 - 1,如果有 减至 0 的,就将它 推入 queue
  • 不再有新的入度 0 的课入列 时,此时 queue 为空,退出循环
**实现代码**:
var findOrder = (numCourses, prerequisites) => {
    let inDegree = new Array(numCourses).fill(0) // 初始化入度数组
    let graph = {} // 哈希表
    for (let i = 0; i < prerequisites.length; i++) {
      inDegree[prerequisites[i][0]]++ // 构建入度数组
      if (graph[prerequisites[i][1]]) { // 构建哈希表
        graph[prerequisites[i][1]].push(prerequisites[i][0])
      } else {
        let list = []
        list.push(prerequisites[i][0])
        graph[prerequisites[i][1]] = list
      }
    }
    let res = [] // 结果数组
    let queue = [] // 存放 入度为0的课
    for (let i = 0; i < numCourses; i++) { // 起初推入所有入度为0的课
      if (inDegree[i] === 0) queue.push(i)
    }
    while (queue.length) { // 没有了入度为0的课,没课可选,结束循环
      let cur = queue.shift() // 出栈,代表选这门课
      res.push(cur) // 推入结果数组
      let toEnQueue = graph[cur] // 查看哈希表,获取对应的后续课程
      if (toEnQueue && toEnQueue.length) { // 确保有后续课程
        for (let i = 0; i < toEnQueue.length; i++) { // 遍历后续课程
          inDegree[toEnQueue[i]]-- // 将后续课程的入度 -1
          if (inDegree[toEnQueue[i]] == 0) { // 一旦减到0,让该课入列
            queue.push(toEnQueue[i])
          }
        }
      }
    }
    return res.length === numCourses ? res : [] // 选齐了就返回res,否则返回[]
  };

总结

  • 解决拓扑排序问题

    1. 根据依赖关系,构建邻接表、和入度数组
    2. 选取入度为 0 的数据,根据邻接表,减小依赖它的数据的入度
    3. 找出新入度为 0 的数据,重复第 2 步
    4. 直至所有数据的入度为 0,得到排序,如果还有数据的入度没有变到 0,说明存在环形依赖
  • 传统BFS 不一样的地方

    • 传统BFS:把出列节点的下一层子节点推入 queue,不加甄别
    • 拓扑排序:实施甄别和监控,新入度为 0 的先推入 queue