重点:
(1)抽象问题:检测一个有向图中是否有环,这个图可能有多个子图;
(2)检测图中是否有环:深搜;拓扑排序(DFS实现);
(3)拓扑排序最优复杂度为O(m+n),一般使用DFS和BFS两种实现拓扑排序;
(4)注意深搜和广搜的差别,一个从后往前,一个从前往后;
难度中等
你这个学期必须选修 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)思路:正向思维,先把入度为0的节点加入答案队列,该节点一定不会有任何入边。当我们把一个新节点加入队列后,移除其所有出边。代表与其相邻的节点少了一门先修课。重复操作,直至所有节点加入队列,说明存在拓扑排序,反之就不存在。
(2)实现:
所需数据结构:队列queue,
建立哈希表存储答案节点,将所有入度为0的节点存入哈希表map,同时加入队列queue。
——依次取队列首部节点,消去边集中的有向边;
——遍历新边集,将新的入度为0的节点入队列;
——队列为空时说明结束,此时检查哈希表中节点个数或者边集个数。也可提前检查队列元素个数加哈希表元素个数之和说明结束。
(1)每次挑选一个未搜索的节点进行深搜;
(2)对于每个进行深搜的节点,顺着路径往下,标记走过的路,发现本条路某个节点重复出现,说明存在环;
注意:此时是从后往前找,找的是每个课程的先修课,所以visit数组可以公用一个,以及为什么最后需要逆序。
- //广度优先搜索
- class Solution {
- public:
- vector<int> findOrder(int numCourses, vector
int >>& prerequisites) { - vector<int> res;
- queue<int> que;
- vector<int> indeg(numCourses,0);
- for(int i=0;i
size();i++) - indeg[prerequisites[i][0]]+=1;
- for(int i=0;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
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
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
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;
- }
- };