每天6道题之第七十一题:课程表(图)

前言

在这里插入图片描述
这两天先不刷之前的题,先把18号要考试的题做一下。

题目描述

你必须选修 numCourse 门课程,记为 0 到 numCourse-1 。 在选修某些课程之前需要一些先修课程。 例如,想要学习课程
0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1] 给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
在这里插入图片描述

题目解析

本题是一道经典的【拓扑排序】问题。
首先,我们来了解一下什么是拓扑排序:
在一个有向图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。
这就和我们这道题一样,就是说在修高阶课程之前,必须要把低阶课程学完。
做法:
先统计所有节点的入度,对于入度为0的借点就可以分离出来,然后把这个节点指向的节点的入度减1。(广度优先搜索)
一直做改操作,直到所有的节点都被分离出来。如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是很多题目的无解的情况。
在这里插入图片描述
了解完拓扑排序之后,我们再来了解一下什么是深度优先,什么是广度优先
(1)二叉树的深度优先遍历的非递归的通用做法是采用栈,广度优先遍历的非递归的通用做法是采用队列。
深度优先遍历(栈):对于每一个可能的分支路径深入到不能再深入为止,也就是不再有出度了,而且每一个结点只能访问一次,需要注意的是,二叉树的深度优先遍历比较特殊,可以细分为先序遍历、中序遍历、后序遍历等。(后面再详细了解)
广度优先遍历(队列):又叫层次遍历,从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有回答可以访问为止。

解题思路

我们可以将本题建模成一个求拓扑排序的问题:
(1)我们将每一门课看成一个节点
(2)如果想要学习课程A之前必须完成课程B,那么我们就从B到A连接一条有向边,这样一来,在拓扑排序中,B一定出现在A的前面。

方法一:深度优先搜索

我们可以将深度优先搜索的流程与拓扑排序的求解联系起来,用一个栈来存储所有已经搜索完成的节点。
(其实用大白话就是一直往下遍历,一直到某一个节点的出度为0,就把该节点入栈,并改变其搜索状态)
算法:
对于图中的任意一个节点,它在搜索的过程中有三种状态:
(1)未搜索
(2)搜索中:我们搜索过这个节点,但是还没有回溯到该节点,也就是该节点还没有入栈,还有相邻的节点没有搜索完成
(3)已完成,我们搜索并回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置。
通过上述的三种状态,我们就可以给出使用深度优先搜索得到拓扑排序的算法流程,在每一轮的搜索搜索开始时,我们任取一个【未搜索】的节点开始进行dfs:
(1)我们将当前搜索的节点u标记为【搜索中】,遍历该节点的每一个相邻节点v:
①如果v为【未搜索】,那么我们开始搜索v,待搜索完成回溯到u。
②如果v为【搜索中】,那么我们就找到了图中的一个环,因此是不存在拓扑排序的
③如果v为【已完成】,那么说明v已经在栈中,而u还不在栈中,因此u无论何时入栈都不会影响到(u,v)之前的拓扑关系,以及不用进行任何操作。
(2)当u的所有相邻接点都为【已完成】时,我们将u放入栈中,并将其标记为【已完成】。

深度优先搜索代码

class Solution{
	private:
		vector<vector<int>> edges;//保存边,每一条边都需要两个节点 
		vector<int> visited;//保存遍历状态0:未搜索   1:搜索中  2:搜索完成 
		bool valid=true; //valid表示当前顺序是否会成环,成环为false,表示不存在拓扑排序 

	
	public:
		void dfs(int u){
			visited[u]=1;//visited=1表示在搜索中 
			for(int v:edges[u]){
				if(visited[v]==0){//visited=0表示未搜索 
					dfs(v);
					if(!valid){
						return ;
					}
				}
				else if(visited[v]==1){
					valid=false;
					return ;
				}
			}
			visited[u]=2; 	
		}

		bool canFinish(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);
				}
			}
			return valid;
			
		}
};

方法一完整代码

#include<vector>
#include<iostream>

using namespace std;

class Solution{
	private:
		vector<vector<int>> edges;//保存边,每一条边都需要两个节点 
		vector<int> visited;//保存遍历状态0:未搜索   1:搜索中  2:搜索完成 
		bool valid=true; //valid表示当前顺序是否会成环,成环为false,表示不存在拓扑排序 

	
	public:
		void dfs(int u){
			visited[u]=1;//visited=1表示在搜索中 
			for(int v:edges[u]){
				if(visited[v]==0){//visited=0表示未搜索 
					dfs(v);
					if(!valid){
						return ;
					}
				}
				else if(visited[v]==1){
					valid=false;
					return ;
				}
			}
			visited[u]=2; 	
		}

		bool canFinish(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);
				}
			}
			return valid;
			
		}
};


int main(){
	vector<vector<int>> prerequisites;//先决条件 
	int numCourses;
	int m;//先决条件的数目
	cin>>numCourses>>m;
	int c1,c2;
	int ch;
	for(int i=0;i<m;i++){
		vector<int> aPrerequisite;
		cin>>c1>>c2;
		aPrerequisite.push_back(c1);
		aPrerequisite.push_back(c2);
		prerequisites.push_back(aPrerequisite);
	} 
	bool res=Solution().canFinish(numCourses,prerequisites);
	cout<<(res?"true":"false")<<endl;
	return 0;
	
	
} 

方法二:广度优先搜索

方法一的深度优先搜索是一种【逆向思维】,最先被放入栈中的结点是在拓扑排序中最后面的节点,我们也可以使用正向思维,顺序的生成拓扑排序。
(其实用大白话就是从上到下,从左到右遍历,如果一个节点的入度为0,就入队列,并改变其状态)
算法:
我们使用一个队列来进行广度优先搜索。初始时,所有入度为0的节点都被放在队列中,他们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的。
在广度优先搜索的每一步中,我们取出队首的节点u:
(1)我们将u放在答案中
(2)我们移除u的所有出边,也就是将u的所有相邻节点的入度减1.如果某个相邻节点v的入度变为0,那么我们就将v放入队列中。
在广度优先搜索的过程结束后,如果答案中包含了这n个结点,那么我们就找到了一种拓扑排序,否则说明图中存在环,不存在拓扑排序

(后面复习的时候再看吧,真难)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值