LeetCode 207.Course Schedule

There are a total of n courses you have to take, labeled from 0 to n - 1.

Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]

Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?

For example:

2, [[1,0]]

There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.

2, [[1,0],[0,1]]

There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.

Note:
1. The input prerequisites is a graph represented by a list of edges,
not adjacency matrices. Read more about how a graph is represented.
2. You may assume that there are no duplicate edges in the input
prerequisites.

题意:从0~n-1的n门课程,有优先次序,比如你要上课程0,必须先上课程1,用pair:[0,1]来表示优先次序。问你能否完成所有课程。
样例:2 [[1,0]]
给出两门课,上课程1必须先上课程0,输出true
样例:2 [[1,0],[0,1]]
上课程1必须先上课程0,上课程0又必须先上课程1,输出false

解题思路:很显然是拓扑排序的题,我们可以根据所有pair,建立图
一种方式可以判断存在不存在环,但是不能输出上课次序
另一种方式可以进行拓扑排序,输出上课次序与否可以根据我们需要。

1、Approach One
拓扑排序(不清楚AOV和拓扑排序的需要自行了解)

class Solution {
public:
    /*    
    * 1. 如何解决这个问题?
    *   先建立AOV网,选择邻接表法建图,之后进行拓扑排序。
    * 2. 理清拓扑排序思路:
    * (1) 从AOV网中选择一个没有前驱的顶点,输出它
    * (2) 从AOV网中删除该顶点,并且删除以该顶点为尾的全部有向边
    * (3) 重复上述两步,直到剩余的网中不再存在没有前驱的顶点为止
    * 3. 如何建图?使用什么数据结构?要注意什么?
    *   数据结构:根据题目给出条件,我们可以使用vector<unordered_set<int>> graph建图
    *   注意:由于要进行拓扑排序,我们建树时要记录每个节点的入度vector<int> indegrees(numCourses, 0)
    *   建图:for pre in prerequisites:    // <0,1> 1 -> 0 : to take course 0 you have to first take course 1
    *           graph[pre.second].insert(pre.first)  // 插入节点
    *           indegrees[pre.first]++;             // 弧尾入度++
    * 4. 拓扑排序
    *   此问题不需要进行边的删除,我们只需要减少节点的入度就可以了
    */ 

    /*  Approach One:拓扑排序不使用栈或者队列   
    *   属于BFS:访问一个顶点,然后访问该顶点的其余邻接点  */
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
        vector<unordered_set<int>> graph(numCourses);   // 模拟邻接表
        vector<int> indegrees(numCourses, 0);       // 用于记录节点入度数
        createGraph(graph, numCourses, prerequisites, indegrees);
        // 拓扑排序
        for(int i = 0; i < numCourses; ++i){
            // 找到入度为0的节点
            int zeroIn = 0;
            for(; zeroIn < numCourses; ++zeroIn)
                if(!indegrees[zeroIn]) break;
            // 没有入度为0的节点了
            if(zeroIn == numCourses) return false;

            // 删除顶点和与顶点相连的边
            // 如 1 -> 2 , 1 -> 3 :1的入度变为-1,而2、3的入度均要减1 
            indegrees[zeroIn] = -1;
            for(int next : graph[zeroIn])
                indegrees[next]--;
        }
        return true;
    }

    void createGraph(vector<unordered_set<int>> &graph, int numCourses
        , vector<pair<int, int>>& prerequisites, vector<int>& indegrees){
        for(pair<int, int>& pre : prerequisites){
            graph[pre.second].insert(pre.first);    // 插入节点
            indegrees[pre.first]++;     // 弧尾入度++       
        }
    }
};

分析时间复杂度和空间复杂度
Time Complexity:建图O(n),拓扑排序O(n^2),拓扑排序没有使用队列和栈,所以总的时间复杂度是O(n^2)
Space Complexity:邻接表O(n+e),n代表顶点数,e代表边数
提交结果:beats 41%
这里写图片描述

2. Approach Two
拓扑排序使用栈:
由于上述代码中,我们每次都需要遍历indegrees中重新找出入度为0的顶点,我们可以使用栈或者队列(自己思考为什么都可以),此处我们使用
1. 先将所有入度为0的顶点入栈
2. 循环从弹出栈顶元素,每次删除栈顶元素的边(即减少弧头的入度, 1 -> 2,2是弧头,indegrees[2]–)时,如果弧头的入度变成了0,将弧头加入栈,之后继续弹出,直至栈空
3. 使用栈还存在一个问题,如果是非连通图,那么我们没有遍历到所有的课程,不能完成所有课程。这个问题我们可以使用一个计数器来计算出栈的元素个数。
注意:如果存在环,会有入度不为0的节点,这样便解决了我们的问题。

/*  Approach Two:拓扑排序使用栈
*   BFS:访问一个顶点,然后访问该顶点的其余邻接点    */
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
    vector<unordered_set<int>> graph(numCourses);           // 模拟邻接表
    vector<int> indegrees(numCourses, 0);       // 用于记录节点入度数
    createGraph(graph, numCourses, prerequisites, indegrees);
    // 拓扑排序
    stack<int> st;
    int counter = 0;  // 记录入度为0的节点数
    // 入度为0的顶点入栈
    for(int i = 0; i < numCourses; ++i)
        if(!indegrees[i]) st.push(i);
    while(!st.empty()){
        int zeroIn = st.top();  // 弹出一个入度为0的顶点
        st.pop();
        ++counter;  // 入度为0的节点数
        /* plan[k++] = zeroIn; // 可以记录课程的安排次序*/
        // 访问zeroIn的所有边
        for(int next : graph[zeroIn]){
            if(!(--indegrees[next]))    //  弧头入度数减1变成了入度为0的顶点 (如1 -> 2,2即弧头)
                st.push(next);
        }
    }
    if(counter == numCourses)   // 拓扑排序之后所有节点都可以入度为0,除非是非连通图或者存在环
        return true;
    else
        return false;
}

分析时间复杂度和空间复杂度:
Time Complexity:建图O(n),第一次入栈O(n),注意到我们访问每个节点,之后都是访问该节点的边,所以时间复杂度为O(3n+e),实际拓扑排序O(2n+e)。哇,其实如果图节点很多边较少,相对于O(n^2),是不是会有很大改进呢?
Space Complexity:邻接表O(n+e),栈O(n)
提交结果:beats 67.16%(哈哈,进步)
这里写图片描述

3、Approach Three
判断是否存在环

class Solution {
public:
    /*  Approach Three:查看是否存在环
    *   Depth First Search:
    *    1. 访问一个顶点w,然后访问该顶点的未被访问的下一个邻接点v
    *    2. 访问v的下一个未被访问的邻接点x,依次类推,直到所有和w相通的点都被访问过
    *    3. 若此时仍有顶点未被访问,则从中选一个顶点做为始点,重复1.2
    *    注意:我们要使用一个visited[0..n-1],标识顶点是否被访问
    *   那么在有向图的DFS中,我们如何判断是否存在环呢?
    *    很简单,我们可以记录我们访问的路径path
    *    假设我们正在访问V,准备访问V的下一个邻接点W,但是W在我们访问的路径上,则存在环
    */
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {         
        // 模拟邻接表
        vector<unordered_set<int>> graph = createGraph(numCourses, prerequisites);
        vector<bool> visited(numCourses, false);    // 被访问过的节点
        vector<bool> path(numCourses, false);  // 当前的访问路径
        for(int i = 0; i < numCourses; ++i)
            if(!visited[i] && dfsCycle(graph, i, visited, path))    // 顶点未被访问过,但存在环
                return false;
        return true;
    }

    bool dfsCycle(vector<unordered_set<int>>& graph, int v, vector<bool>& visited, vector<bool>& path){
        if(visited[v]) return false;    // 被访问过的节点不需要再次被访问
        path[v] = visited[v] = true;    // 设置顶点为被访问过,加入当前的访问路径
        for(int next : graph[v]){
            // 如果该顶点的邻接点在当前访问路径上,说明存在环,否则查看邻接点
            if(path[next] || dfsCycle(graph, next, visited, path))
                return true;
        }
        path[v] = false;    // 递归回来时,我们需要将该顶点取出访问路径
        return false;
    }

    vector<unordered_set<int>> createGraph(int numCourses, vector<pair<int, int>>& prerequisites){
        vector<unordered_set<int>> graph(numCourses);
        for(pair<int, int>& pre : prerequisites)
            graph[pre.second].insert(pre.first);    // 插入节点
        return graph;
    }
};

提交结果:16ms beats 67.16%
这里写图片描述

(发现写代码之前把思路厘清是很必要的,这几次写的代码都是直接过的,惊讶到自己,哈哈)

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值