课程表

题目描述

来源于 https://leetcode-cn.com/

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

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

给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?

示例 1:

输入: 2, [[1,0]] 
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。

示例 2:

输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。

说明:

  1. 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法
  2. 你可以假定输入的先决条件中没有重复的边。

提示:

  1. 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
  2. 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
  3. 拓扑排序也可以通过 BFS 完成。

解法一:

课程之间互相依赖,构成一个有向图。只要这个有向图中没有环,即不存在循环依赖,那么所有课程都能够修完。

遍历图,并记录下图中节点是否访问过,当一个节点被两次访问到的时候,说明存在环。遍历使用 DFS 或者 BFS 均可。这里我使用了两个标记 visitedonstack,其中 visited[i] = true 表示节点 i 在某次遍历中访问过了,而 onstack[i] = true 表示节点 i 在本次深度优先遍历中访问过了。

因为深度优先遍历要从所有的顶点都进行一次,只有在其中一次深度优先遍历中发现某个点被访问多次,才证明存在环。另外,其中一个点可能会指向另外一个子图,之前这个子图可能已经遍历过了,如果不使用 visited 来标记,就会重复遍历。

class Solution {
private:
    bool has_cycle = false;
    unordered_map<int, unordered_set<int>> graph;
    vector<bool> visited;
    vector<bool> onstack;
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        graph.clear();
        graph.reserve(numCourses);
        visited.resize(numCourses);
        onstack.resize(numCourses);

        for(auto& edge: prerequisites){
            int w = edge[0];
            int v = edge[1];
            graph[v].insert(w);
        }

        for(auto & item : graph){
            int v = item.first;
            if(!visited[v]){
                dfs(v);
            }
        }
        return !has_cycle;
    }

private:
    void dfs(int v){
        visited[v] = true;
        onstack[v] = true;
        unordered_set<int> &nodes = graph[v];
        for(int w: nodes){
            if(has_cycle){
                return;
            }
            if(!visited[w]){
                dfs(w);
            }else if(onstack[w]){
                has_cycle = true;
            }
        }
        onstack[v] = false;
    }
};

解法二:

先把那些不需要前驱课程的课修了,然后进一步修依赖这些课的其他课程,而后继续。最后判断能够修的课程和全部课程是否相等。

不需要前驱课程,就是图中入度为 0 的点。当一个入度为 0 的点,消除后,该点指向的节点的入度就减 1,那么节点如果入度减为 0,那就可以进一步消除其他节点。

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        unordered_map<int, int> in_degrees(numCourses);
        unordered_map<int, unordered_set<int>> graph(numCourses);
        for(auto& item: prerequisites){
            graph[item[1]].insert(item[0]);
            in_degrees[item[0]] += 1;
        }

        queue<int> zero_dep_queue;
        for(int i=0;i<numCourses;i++){
            if(in_degrees[i] == 0){
                zero_dep_queue.push(i);
            }
        }

        int count = 0;
        while (!zero_dep_queue.empty()){
            int w = zero_dep_queue.front();
            zero_dep_queue.pop();
            count += 1;

            for(int v: graph[w]){
                in_degrees[v] -= 1;
                if (in_degrees[v] == 0){
                    zero_dep_queue.push(v);
                }
            }
        }

        return count == numCourses;
    }
};