leetcode:207. 课程表

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {

    }
};

题目解析

思考:什么时候无法修习完所有课程呢?当存在循环依赖的时候。也就是存在环。即所有课程应该组成一个有向无环图。

因此,第一步,我们应该将问题转换为一个[有向图]这样的数据结构,只要图中存在环,那么就表示它存在循环依赖。

怎么将问题转换为[有向图]呢?

  • 我们先将课程看成是[有向图]中的节点,节点编号分别是 0, 1, …, numCourses-1,把课程之间的依赖关系看做节点之间的有向边。
  • 怎样表示这张图呢?图的表示方法一般来说有两种,邻接矩阵、邻接链表等。这里我们用邻接矩阵

怎么判断图是否存在环呢?方法有很多,比如说拓扑排序、BFS、DFS等等。

这里我们来看看DFS怎么写。

  • 首先根据课程表生成一张有向图,这里我们用一个二维数组来表示邻接拒绝
  • 我们用一个一维数组 visit 来记录访问状态,这里有三种状态,0表示还未访问过,1表示已经访问了,-1 表示有冲突。
  • 然后以每一门课的起点,找它的所有前置依赖是否会出现环
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
    	// 用二维数组来表示邻接矩阵
        vector<vector<int>> graph(numCourses, vector<int>());
        vector<int> visit(numCourses);
        // 
        for (auto a : prerequisites) {
            graph[a[1]].push_back(a[0]);  // a[1]的前置课程是a[0]
        }
        for (int i = 0; i < numCourses; ++i) {
        	// 遍历图中所有的节点
            if (!canFinishDFS(graph, visit, i)) return false;
        }
        return true;
    }
    bool canFinishDFS(vector<vector<int>>& graph, vector<int>& visit, int i) {
        if (visit[i] == -1) return false;  //出现了环
        if (visit[i] == 1) return true;  //以canFinish中的for循环之前的循环已经找过了,当前numCourses[i]的前置课程不会出现环
        visit[i] = -1;  // 以canFinish中的for循环为起点寻找环时,如果再一次访问numCourses[i],那么就表示出现了环
        for (auto a : graph[i]) {
            if (!canFinishDFS(graph, visit, a)) return false;
        }
        visit[i] = 1;
        return true;
    }
};

怎么统计其拓扑排序呢?

  • 申请一个queue:(只有那些入度为0的节点才读取)
    • 遍历图中每一个节点,将入度为 0 的节点入队
  • 申请一个vector,将生成的拓扑序一次压入vec中
  • 如果队列不为空(根据队列循环执行“删边”操作):
    • 每次从队列头部取出一个节点,并压入vector
    • 然后将这个节点所有邻居节点的入度减1(相当于删边)
    • 如果减到0,就把该邻居节点推入队列,表示下一轮要优先定位的节点。

拓扑排序可以判断有向图是否有环。

  • 如果最终生成的拓扑排序的长度 刚好就是节点个数,说明没有环,直接返回true
  • 如果有环的话,那么在所有节点被加入队列之前队列就会空(画一下图就能看出来)。
#include <iostream>
#include <vector>
#include <random>
#include <iterator>
#include <map>
#include <algorithm>
#include <set>
#include <unordered_map>
#include <queue>

using namespace std;

struct Node;
struct Edge;
struct Graph;

typedef struct Node{
    int val;
    int in;
    int out;
    
    std::vector<Node *> nexts; // 存储的是可以走到下一个的节点
    std::vector<Edge *> edges;  // 存储的是指向下一个的方向
    
    Node(int v, int i = 0, int o = 0) : val(v), in(i), out(o){
    }
} Node;

typedef struct Edge{
    int weight;
    Node *from;
    Node *to;
    
    Edge(int w, Node *f, Node *t) : weight(w), from(f), to(t){
    }
} Edge;

struct Graph{
    std::map<int, Node *> nodes;
    std::set<Edge *> edges;
};

class Solution{
    Graph * createGraph(std::vector<std::vector<int>> p){
        auto g = new Graph();
        int N = p.size();
        for (int i = 0; i < N; ++i) {
            int fLabel = p[i][0], tLabel = p[i][1];
            if(g->nodes.count(fLabel) == 0){
                g->nodes[fLabel] = new Node(fLabel);
            }
            if(g->nodes.count(tLabel) == 0){
                g->nodes[tLabel] = new Node(tLabel);
            }
            auto fNode = g->nodes[fLabel], tNode = g->nodes[tLabel];
            auto nEdge = new Edge(0, fNode, tNode);
            fNode->out++;
            tNode->in++;
            fNode->nexts.emplace_back(tNode);
            fNode->edges.emplace_back(nEdge);
            g->edges.emplace(nEdge);
        }
        return g;
    }
    
    int topSortLength(Graph *g){
        if(g == nullptr){
            return 0;
        }
        
        std::queue<Node*> zeroQueue;
        
        for(auto it : g->nodes){
            auto node = it.second;
            if(node->in == 0){
                zeroQueue.emplace(node);
            }
        }
        
        std::vector<Node *> vec;
        while (!zeroQueue.empty()){
            auto top = zeroQueue.front(); zeroQueue.pop();
            vec.emplace_back(top);
            for(auto next : top->nexts){
                --next->in;
                if(next->in == 0){
                    zeroQueue.emplace(next);
                }
            }
        }
        
        return vec.size();
    }

public:
    bool canFinish(int numCourses, vector<vector<int>>& p){
        if(numCourses == 0 || p.empty()){
            return true;
        }
        
        auto graph = createGraph(p);
        return topSortLength(graph) == graph->nodes.size();
    }
};

简化写法

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector<vector<int>> graph(numCourses, vector<int>());
        vector<int> in(numCourses);
        for (auto a : prerequisites) {
            graph[a[1]].push_back(a[0]);
            ++in[a[0]];
        }
        queue<int> q;
        for (int i = 0; i < numCourses; ++i) {
            if (in[i] == 0) q.push(i);
        }
        while (!q.empty()) {
            int t = q.front(); q.pop();
            for (auto a : graph[t]) {
                --in[a];
                if (in[a] == 0) q.push(a);
            }
        }
        for (int i = 0; i < numCourses; ++i) {
            if (in[i] != 0) return false;
        }
        return true;
    }
};

类似题目

题目思路
leetcode:207. 能不能完成课程表 course-schedule如果拓扑排序的长度刚好就是节点个数,那么就返回true; 如果是用邻接矩阵,那么需要消息nums是从0开始还是从1开始的,不能靠统计一共上了多少课来看是否形成环。 如果是有向图的话,用链表表示图比较不容易出错
leetcode:210. 能学完所有课程的正确顺序 II course-schedule ii返回任意一个拓扑顺序。易错点:必须保证能够形成top排;有些课程可能没有前置课程。 如果是有向图的话,用链表表示图比较不容易出错
leetcode:630. 最多能修习的课程数量 III course-schedule iii贪心:优先学习截止时间更近的、学习时间较短的。用优先队列维护
leetcode:1136. 修完全部课程至少需要几个学期拓扑排序的层数&&必须能形成拓扑排序
leetcode:310. 最小高度树Minimum Height Trees 思路:一层一层的褪去叶节点(只有一条边的是叶子节点),最后剩下的一个或两个节点就是我们要求的最小高度树的根节点。 因为它是无向图,所以用邻接矩阵比较方便
leetcode:802. 找到最终的安全状态 find-eventual-safe-states
leetcode:133. 克隆图 Clone Graph
leetcode:269. 火星词典 Alien Dictionary
leetcode:444. 序列重建Sequence Reconstruction
leetcode:261. 以图判树 Graph Valid Tree
leetcode:802. 找到最终的安全节点 find-eventual-safe-states
算法:图常见算法的实现
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值