题目
两个题非常相似,只记录210题
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。说明:
- 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule-ii
概述
207题是图的环形检测,210题是图的拓扑排序。都比较经典。
但是由于图的代码写的比较少,所以也是费了一些时间,同时最近在做的项目中用到了图计算,所以正好回顾一下这部分内容。
总结
做题过程中的一些思考,整理成一些通用的概念,记录在这里。
1、图的表示方法有很多,常见的有邻接矩阵、邻接表、边缘列表。参见文章
2、图的搜索方法有深度优先和广度优先,这两种方法都是站在点的角度,以边作为方向去进行的,所以进行bfs和dfs首先需要用邻接矩阵或者邻接表的方式表示图,边缘列表是不行的。
3、环形检测,可以用拓扑排序的方式,依次找入度为0的点,如果在所有点遍历完成之前不存在入度为0的点,就说明存在环;也可以用dfs进行图遍历,如果在遍历过程中存在后向边,也就是边指向了遍历路径中的父节点,也说明存在环。两者的原理一致,根据不同的定理实现了不同的方法。
其实这两种方法分别对应了bfs和dfs。可以认为环形检测和拓扑排序是两类问题,都可以用dfs和bfs实现。(dfs实现拓扑排序的原理就是从一个点开始进行dfs,过程中每个点被标记为未搜索、搜索中、搜索完,所以被标记为搜索完的点一定是可以放在所有未搜索和搜索中的点之后的,将其入栈,最终栈里的点一定就是拓扑顺序的)
分别用bfs和dfs实现210题,代码如下:
dfs:
import java.util.*;
public class Test {
// 用邻接表来表示图
List<List<Integer>> neighbourTable;
// 每个节点的搜索状态:0代表未搜索、1代表正在搜索、2代表已经搜索完成
int[] status;
// 保存结果的栈
Stack<Integer> stack;
// 图是否有效,也就是是否无环
boolean valid;
public int[] findOrder(int numCourses, int[][] prerequisites) {
neighbourTable = new ArrayList<>(numCourses);
status = new int[numCourses];
stack = new Stack<>();
valid = true;
//
for (int i = 0; i < numCourses; i++) {
neighbourTable.add(new ArrayList<>());
}
// 依次遍历所有未搜索过的点
for (int i = 0; i < numCourses && valid; i++) {
if (status[i] == 0) {
dfs(i);
}
}
// 返回结果,栈中的数据依次出栈,就是拓扑排序的顺序
if (valid) {
int[] result = new int[numCourses];
int resultIndex = 0;
while (!stack.isEmpty()) {
result[resultIndex++] = stack.pop();
}
return result;
} else {
return new int[0];
}
}
public void dfs(int i) {
// 将节点标记为搜索中
status[i] = 1;
// 该节点的所有邻接点进行dfs,如果检测到图有环,及时中止
for (int neighbour : neighbourTable.get(i)) {
if (status[neighbour] == 1) {
valid = false;
return;
} else if (status[neighbour] == 0) {
dfs(neighbour);
if (!valid) return;
}
}
// 将节点标记为搜索完,并放入栈中
stack.push(i);
status[i] = 2;
}
}
bfs,也就是利用入度为0进行拓扑排序:
import java.util.*;
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 用邻接表存储图结构
Map<Integer, Set<Integer>> neighbourTable = new HashMap<>();
// 记录所有点的入度
Map<Integer, Integer> vertexInDegree = new HashMap<>();
// 初始化
for (int i = 0; i < numCourses; i++) {
neighbourTable.put(i, new HashSet<>());
vertexInDegree.put(i, 0);
}
// 边缘列表转化成邻接表,并计算所有点的入度
for (int[] edge : prerequisites) {
neighbourTable.get(edge[1]).add(edge[0]);
vertexInDegree.put(edge[0], vertexInDegree.getOrDefault(edge[0], 0) + 1);
}
// 寻找入度为0的点,并用队列维持
Queue<Integer> zeroInDegreeVertexes = new LinkedList<>();
for (int vertex : vertexInDegree.keySet()) {
if (vertexInDegree.get(vertex) == 0) {
zeroInDegreeVertexes.add(vertex);
}
}
// 结果数组,以及结果指针
int[] result = new int[numCourses];
int resultIndex = 0;
// 从入度为0的数组中获取点,宽度优先遍历其所有邻接点,入度减1,并及时将入度为0的节点入队列
while (!zeroInDegreeVertexes.isEmpty()) {
int vertex = zeroInDegreeVertexes.poll();
result[resultIndex++] = vertex;
for (int neighbourVertex : neighbourTable.get(vertex)) {
vertexInDegree.put(neighbourVertex, vertexInDegree.get(neighbourVertex) - 1);
if (vertexInDegree.get(neighbourVertex) == 0) {
zeroInDegreeVertexes.add(neighbourVertex);
}
}
}
return resultIndex == numCourses ? result : new int[0];
}
}
时间复杂度:O(v+e)
空间复杂度:O(v+e)
耗时:2小时