拓扑排序

拓扑排序的原则在于,每次找到一个没有入边的节点,将其输出后并标记(或者输出后删除,并删除所有其出边,是一个意思)。
当然最后结果看需要的排序方向,如果是反着来的,也可以将入边和出边换一下,不影响结果。
因此寻找这个没有出边的节点,就可以三种方式,一种是DFS(深度优先搜索),一种是BFS(广度优先搜索)。其中BFS算法又可以称为Kahn算法。

例子

这是Leetcode中的一道典型的拓扑排序题。

现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: 2, [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
     因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

DFS

其实拓扑排序的本质就是,在DFS模板的基础上,将每次DFS尽头的点(这个点是没有出度的点)放入结果中。考虑到可能有环的情况,因此需要用valid和mark的额外标记位来判断。

class Solution {
    List<Integer>[] edges;
    // 0 not visited, 1 visiting for circle detect, 2 visited
    int[] mark;
    // has circle
    boolean valid = true;
    int res[];
    int resIndex = 0;
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // read data
        edges = new List[numCourses];
        res = new int[numCourses];
        mark = new int[numCourses];
        // fill empty to lists
        for(int i = 0; i < numCourses; i++){
            edges[i] = new ArrayList<>();
        }
        // read edges
        for(int[] edge : prerequisites){
            int to = edge[1];
            int from = edge[0];
            List<Integer> l = edges[from];
            l.add(to);
        }
        for(int i = 0; i < numCourses; i++){
            if(mark[i] == 0){
                dfs(i);
                if(!valid){
                    return new int[0];
                }
            }
        }
        return res;
    }

    public void dfs(int i){
        if(mark[i] == 2){
            return;
        }
        mark[i] = 1;
        List<Integer> tos = edges[i];
        for(int to : tos){
            if(mark[to] == 1){
                valid = false;
                return;
            }
            dfs(to);
            if(!valid){
                return;
            }
        }
        mark[i] = 2;
        res[resIndex++] = i;
    }
}

Kahn(BFS)

Kahn算法的思路是,先将左右的没有入边的节点加入队列,每次从队列中取出一个没有入边的节点,删除这个节点及其所有出边,并放入结果中。然后遍历其所有出边的对应节点(此时这些节点的入边数量就少1),把所有没有入边的节点放入队列中。这就重复了直到所有点都放入结果中。
在图中有环时,会有节点永远不会被加入到队列中,因此算法结束后结果集数量少于节点数量。

class Solution {
    int[] indegree;
    // nodes which indegree is zero
    Queue<Integer> setOfZeroIndegree;
    List<Integer>[] edges;
    // result set
    int[] res;
    int resIndex = 0;
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // init
        indegree = new int[numCourses];
        res = new int[numCourses];
        setOfZeroIndegree = new LinkedList<>();
        edges = new List[numCourses];
        for(int i = 0; i < numCourses; i++){
            edges[i] = new ArrayList<>();
        }

        for(int[] edge : prerequisites){
            int to = edge[0];
            int from = edge[1];
            edges[from].add(to);
            indegree[to]++;
        }

        for(int i = 0; i < numCourses; i++){
            if(indegree[i] == 0){
                setOfZeroIndegree.add(i);
            }
        }

        int cur;
        while(!setOfZeroIndegree.isEmpty()){
            cur = setOfZeroIndegree.poll();
            res[resIndex++] = cur;
            // for all to nodes, indegree sub one. if nodes has no indegree, put into setOfZeroIndegree.
            for(int i : edges[cur]){
                indegree[i]--;
                if(indegree[i] == 0){
                    setOfZeroIndegree.add(i);
                }
            }
        }
        // graph has circle
        if(resIndex != numCourses){
            return new int[0];
        }
        return res;
    }
}
上一篇:【leetcode】课程表


下一篇:210. Course Schedule II