拓扑排序
参考文章:
拓扑排序,YYDS
Java 简单好理解的拓扑排序
1. 简介
对一个有向无环图(Directed Acyclic Graph简称DAG) G 进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。如图所示
接下来通过一道题 课程表II ,来理解拓扑排序的应用场景和使用方法。
2. 课程表II
- 题目描述
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,
其中 prerequisites[i] = [a, b] ,表示在选修课程 a 前 必须 先选修 b 。
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。
如果不可能完成所有课程,返回 一个空数组 。
- 实例
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
- 解题思路
可以把每一个课程看成一个点,prerequisites[i]
看成一条边。那么它们就是一副图。给出学习顺序,是不是就是将所有的课程排成一个线性序列。因此,可以判断这道题就是用拓扑排序来解决的。
- 首先需要根据
prerequisites
构建一个图(一般是用邻接表形式来构建图)
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
//graph[i]存储着节点i所指向的节点
List<Integer>[] graph = new LinkedList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
for (int[] edge : prerequisites) {
int from = edge[1];
int to = edge[0];
// 修完课程 from 才能修课程 to
// 在图中添加一条从 from 指向 to 的有向边
graph[from].add(to);
}
return graph;
}
- 遍历图
// 防止重复遍历同一个节点
boolean[] visited;
// 从节点 s 开始 BFS 遍历,将遍历过的节点标记为 true
void traverse(List<Integer>[] graph, int s) {
//如果节点已经访问过,直接返回
if (visited[s]) {
return;
}
/* 前序遍历代码位置 */
// 将当前节点标记为已遍历
visited[s] = true;
for (int t : graph[s]) {
traverse(graph, t);
}
/* 后序遍历代码位置 */
}
- 判断图中有没有环
//记录当前traverse经过的路径
boolean[] onPath;
//是否有环
boolean hasCycle = false;
//记录当前节点是否访问过
boolean[] visited;
//遍历图
void traverse(List<Integer>[] graph, int s) {
//如果节点又一次被访问到,说明有环
if (onPath[s]) {
hasCycle = true;
}
if (visited[s]) {
return;
}
// 将节点 s 标记为已遍历
visited[s] = true;
// 开始遍历节点 s
onPath[s] = true;
for (int t : graph[s]) {
traverse(graph, t);
}
// 节点 s 遍历完成
onPath[s] = false;
}
所有的准备工作都完成了,接下来直接给出答案。
DFS题解
//记录遍历过的节点,防止重复遍历同一个节点
boolean[] visited;
//记录一次traverse递归经过的节点
boolean[] onPath;
//记录图中是否有环
boolean hasCycle = false;
//用来记录课程顺序
List<Integer> postorder = new ArrayList<>();
public int[] findOrder(int numCourses, int[][] prerequisites) {
//构建图
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
//因为图中不是所有节点都相连,所以要遍历每一个节点
for (int i = 0; i < numCourses; i++) {
traverse(graph,i);
}
//如果有环,就不能实现拓扑排序,直接返回
if (hasCycle)
return new int[]{};
Collections.reverse(postorder);
//将postorder转换成int数组,stream是 java 8新特性,不熟悉的可以查一下
return postorder.stream().mapToInt(Integer::intValue).toArray();
}
//构建图
private List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
//使用邻接表的方式来构建图
List<Integer>[] graph = new ArrayList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new ArrayList<>();
}
for (int[] prerequisite : prerequisites) {
int from = prerequisite[1];
int to = prerequisite[0];
graph[from].add(to);
}
return graph;
}
//遍历图
public void traverse(List<Integer>[] graph,int s){
if (onPath[s])
hasCycle = true;
if (visited[s] || hasCycle)
return;
//将当前节点标记为已遍历
visited[s] = true;
//开始遍历节点
onPath[s] = true;
for (Integer integer : graph[s]) {
traverse(graph,integer);
}
//节点遍历结束
onPath[s] = false;
//后续遍历位置,不理解这里的。可以去看文章开头的参考文章。里边有详细讲解
postorder.add(s);
}
}
BFS题解
思路
- 建立入度表,入度为 0 的节点先入队
- 当队列不为空,节点出队,标记学完课程数量的变量加 1,并记录该课程
- 将课程的邻居入度减 1
- 若邻居课程入度为 0,加入队列
public int[] findOrder(int numCourses, int[][] prerequisites) {
if (numCourses == 0)
return new int[0];
//建立邻接表
List<Integer>[] graph = new LinkedList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
//建立入度表,即有几个节点指向此节点
int[] inDegrees = new int[numCourses];
for (int[] p : prerequisites) {
int from = p[1];
int to = p[0];
//添加一条from到to的有向边
graph[from].add(to);
//p[1]指向p[0],所以p[0]的入度加一
inDegrees[to]++;
}
// 将入度为0的节点加入队列中
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < inDegrees.length; i++) {
if (inDegrees[i] == 0)
queue.offer(i);
}
//记录学完的课程数量
int count = 0;
//记录学完的课程
int[] res = new int[numCourses];
//根据提供的先修课列表,删除入度为 0 的节点
while (!queue.isEmpty()){
int curr = queue.poll();
//将可以学完的课程加入结果当中
res[count++] = curr;
//获得学完课程的相邻节点
List<Integer> part = graph[curr];
for (int p : part) {
//入度减 1
inDegrees[p]--;
//如果入度为0,就可以学习该课程,加入队列
if (inDegrees[p] == 0)
queue.offer(p);
}
}
if (count == numCourses)
return res;
//如果count != numCourses,说明存在环,不能修完所有课程
return new int[0];
}
PS
个人觉得 bfs题解更容易理解,建议掌握。