【算法】拓扑排序总结

652 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

一、前言

拓扑序: 如果图中从 VW 有一条有向路径,则 V 一定排在 W 之前。满足此条件的顶点列称为一个拓扑序。

拓扑排序: 获得一个拓扑序的过程就是拓扑排序。

AOVActivity On Vertex)网络: 如果有合理的拓扑序,则必定是有向无环图(Directed Acyclic Graph,DAG)。


容易想到暴力法:时间复杂度 T= O(|V|^2)

// 伪代码如下:
void topSort() {
    for (int cnt = 0; cnt < |V|; ++cnt) {
        V = 未输入的入度为 0 的顶点 // O(|V|)
        if (这样的 V 不存在) {
            Error("图中有回路");
            break;
        }
        输出 v, 或者记录 v 的输出序号
        for ( v 的每个邻接点 w) {
            Indegree[w] --; // 入度 - 1
        }
    }
}

聪明的算法:随时将入度为 0 的顶点放到一个容器里

时间复杂度 T= O(|V| + |E|)

// 用来检测有向图是否 DAG
void topSort() {
    for (图中每个顶点 v ) {
        if (Indegree[v] == 0) Enqueue(v, Q); // 放入容器
    }
    while (!isEmpty(Q)) {
        v = Dequeue(Q);
        // 输出 v,或者记录v 的输出序号
        ++cnt; // 统计节点
        for (v 的每个邻接点 w) {
            if (--Indegree[w] == 0) {
                Enqueue(w, Q); // 放入容器
            }
        }
    }
    if (cnt != |V|) {  // 统计节点数 是否等于图中所有节点数
        Error("图中有回路");
    }
}

二、题目

(1)课程安排

LeetCode 207. 课程表

题目描述

题目说的是:你有 n 门课要上,课程编号从 0 到 n-1。

在上某些课之前你需要先上另外一些课程,这种依赖关系可以用一个数对来表示。 比如 (0,1) 数对表示在上课程 0 之前,你需要先上课程 1。现在给你 n 门课以及它们之间的依赖关系,你要判断是否有可能上完所有课程。

比如说,给你的课程数量 n 等于 5:
​
n = 5
​
课程之间的依赖关系是:
​
(1, 0)
(3, 0)
(3, 1)
(2, 1)
(2, 3)
(4, 2)
(4, 3)
​
我们只要按照 0, 1, 3, 2, 4 的顺序来排课,就可以在满足依赖关系的条件下完成这 5 门课。因此返回 true。
​
提示:上述依赖关系可以转成以下有向图
​
 0 ---> 3 ---> 4
  \    ^ \    ^
   \  /   \  /
    v/     v/
    1 ---> 2

思路解法

此题意:判断有向图里是否有环

习惯用法:节点总数是 V,边的总数是 E

此题有两种解法:

  1. 方法一:DFS + 回溯:每个节点遍历一次
  2. 方法二:拓扑排序

方法一:DFS + 回溯

这个比较好理解,看图吧。

  1. 构建邻接表
  2. 遍历每个节点

2022-07-1820-49-39.png

public class LeetCode_207 {
​
    // 方法一: dfs + 回溯
    // Time: O(V + E), Space: O(V + E), Faster: 64.27%
    public boolean canFinishDFS(int numCourses, int[][] prerequisites) {
​
        if (numCourses <= 1 || prerequisites == null) return true;
​
        // 构建图: 构建邻接表
        List<List<Integer>> graph = new ArrayList<>(numCourses);
        for (int i = 0; i < numCourses; ++i) {
            graph.add(new LinkedList<>());
        }
        for (int[] pair : prerequisites) {
            graph.get(pair[1]).add(pair[0]);
        }
​
        // 记录这个节点之后是否有环, true 无环 false 可能有环
        boolean[] checked = new boolean[numCourses];
        // 标记是否访问过
        boolean[] visited = new boolean[numCourses];
​
        for (int i = 0; i < numCourses; ++i)
            if (!checked[i] && hasCycle(graph, visited, checked, i))
                return false;
​
        return true;
    }
​
    private boolean hasCycle(List<List<Integer>> graph, boolean[] visited,
                             boolean[] checked, int v) {
​
        if (visited[v]) return true;
​
        visited[v] = true;
​
        for (int i : graph.get(v)) {
​
            if (!checked[i] && hasCycle(graph, visited, checked, i))
                return true;
        }
​
        checked[v] = true;
        visited[v] = false;
​
        return false;
    }
}

方法二:拓扑排序

  1. 构建邻接表
  2. 记录每个节点的入度
  3. 遍历:计算入度为0的节点,将其出度减去
  4. 是否有环:即入度为0的节点数 == 总节点数

2022-07-1821-18-47.png

AC 代码:

public class LeetCode_207 {
​
    // 方法二: 拓扑排序
    // Time: O(V + E), Space: O(V + E), Faster: 39.75%
    public boolean canFinishTopSortAdjList(int numCourses, int[][] prerequisites) {
​
        if (numCourses <= 1 || prerequisites == null || prerequisites.length == 0)
            return true;
​
        // 记录每个节点的入度数
        int[] inDegree = new int[numCourses];
​
        // 构建邻接表
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; ++i) {
            graph.add(new LinkedList<>());
        }
        // 建立节点的关系
        for (int[] pair : prerequisites) {
            graph.get(pair[1]).add(pair[0]);
            ++inDegree[pair[0]];
        }
​
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < inDegree.length; ++i) {
            if (inDegree[i] == 0) {
                q.add(i);
            }
        }
​
        int count = 0;
        while (!q.isEmpty()) {
            int v = q.poll();
            ++count;
            for (int i : graph.get(v)) {
                --inDegree[i];
                if (inDegree[i] == 0) q.add(i);
            }
        }
        return count == numCourses;
    }
}