重点:
(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、广搜优先搜索
(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;
}
};