前言
这两天先不刷之前的题,先把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个结点,那么我们就找到了一种拓扑排序,否则说明图中存在环,不存在拓扑排序
(后面复习的时候再看吧,真难)