【力客热题HOT100】-【061】207 课程表

重点:

(1)抽象问题:检测一个有向图中是否有环,这个图可能有多个子图;

(2)检测图中是否有环:深搜拓扑排序(DFS实现)

(3)拓扑排序最优复杂度为O(m+n),一般使用DFS和BFS两种实现拓扑排序;

(4)注意深搜和广搜的差别,一个从后往前,一个从前往后

207. 课程表

难度中等

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

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

示例 2:

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

提示:

  • 1 <= numCourses <= 105
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

解析:

不难想到,本题考查有向图中是否存在环。一般用两种方法解决,深搜和拓扑排序,本文重点讲拓扑排序

给定一个包含n个节点得有向图G,我们给出其节点编号的一种排列,如果满足:

对于图中任意一条有向边(u, v),u在排列里并且u都出现在v的前面。

那么称排列是图G的拓扑排序。根据上述定义,我们得出两个结论:

       ——如果G中存在环,那么图G不存在拓扑排序;

       ——如果G是有向无环图,那么其拓扑排序可能不止一种;

有了上述知识,我们可以将此题简单抽象成一个拓扑排序问题。

       ——将每门课看成一个节点;

       ——学习A之前学B,那么便存在有向边(B,A);

理论上来讲,拓扑排序的最最优复杂度为O(m+n),但不可能存在一种该复杂度算法找到拓扑排序。我们一般使用深搜和广搜两种算法实现拓扑排序

1、广搜优先搜索

(1)思路:正向思维,先把入度为0的节点加入答案队列,该节点一定不会有任何入边。当我们把一个新节点加入队列后,移除其所有出边。代表与其相邻的节点少了一门先修课。重复操作,直至所有节点加入队列,说明存在拓扑排序,反之就不存在。

(2)实现

所需数据结构:队列queue,

建立哈希表存储答案节点,将所有入度为0的节点存入哈希表map,同时加入队列queue。

        ——依次取队列首部节点,消去边集中的有向边;

        ——遍历新边集,将新的入度为0的节点入队列;

        ——队列为空时说明结束,此时检查哈希表中节点个数或者边集个数。也可提前检查队列元素个数加哈希表元素个数之和说明结束。

2、深度优先搜索

(1)每次挑选一个未搜索的节点进行深搜

(2)对于每个进行深搜的节点,顺着路径往下,标记走过的路,发现本条路某个节点重复出现,说明存在环;

注意:此时是从后往前找,找的是每个课程的先修课,所以visit数组可以公用一个,以及为什么最后需要逆序。

代码:

//广度优先搜索
class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> res;
        queue<int> que;
        vector<int> indeg(numCourses,0);
        for(int i=0;i<prerequisites.size();i++)
            indeg[prerequisites[i][0]]+=1;
        for(int i=0;i<numCourses;i++)
            if(indeg[i]==0)
                que.push(i);

        //队列为空结束
        while(!que.empty()){
            int tmp=que.front();
            que.pop();
            res.push_back(tmp);
            for(int i=0;i<prerequisites.size();i++){
                if(prerequisites[i][1]==tmp){
                    indeg[prerequisites[i][0]]-=1;
                    if(indeg[prerequisites[i][0]]==0)
                        que.push(prerequisites[i][0]);
                }      
            }
        }
        if(res.size()==numCourses)
            return res;
        else
            return {};
    }
};
class Solution {
private:
    // 存储有向图
    vector<vector<int>> edges;
    // 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
    vector<int> visited;
    // 用数组来模拟栈,下标 0 为栈底,n-1 为栈顶
    vector<int> result;
    // 判断有向图中是否有环
    bool valid = true;

public:
    void dfs(int u) {
        // 将节点标记为「搜索中」
        visited[u] = 1;
        // 搜索其相邻节点
        // 只要发现有环,立刻停止搜索
        for (int v: edges[u]) {
            // 如果「未搜索」那么搜索相邻节点
            if (visited[v] == 0) {
                dfs(v);
                if (!valid) {
                    return;
                }
            }
            // 如果「搜索中」说明找到了环
            else if (visited[v] == 1) {
                valid = false;
                return;
            }
        }
        // 将节点标记为「已完成」
        visited[u] = 2;
        // 将节点入栈
        result.push_back(u);
    }

    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        edges.resize(numCourses);
        visited.resize(numCourses);
        for (const auto& info: prerequisites) {
            edges[info[1]].push_back(info[0]);
        }
        // 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
        for (int i = 0; i < numCourses && valid; ++i) {
            if (!visited[i]) {
                dfs(i);
            }
        }
        if (!valid) {
            return {};
        }
        // 如果没有环,那么就有拓扑排序
        // 注意下标 0 为栈底,因此需要将数组反序输出
        reverse(result.begin(), result.end());
        return result;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值