拓扑排序的原则在于,每次找到一个没有入边的节点,将其输出后并标记(或者输出后删除,并删除所有其出边,是一个意思)。
当然最后结果看需要的排序方向,如果是反着来的,也可以将入边和出边换一下,不影响结果。
因此寻找这个没有出边的节点,就可以三种方式,一种是DFS(深度优先搜索),一种是BFS(广度优先搜索)。其中BFS算法又可以称为Kahn算法。
例子
这是Leetcode中的一道典型的拓扑排序题。
现在你总共有 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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
DFS
其实拓扑排序的本质就是,在DFS模板的基础上,将每次DFS尽头的点(这个点是没有出度的点)放入结果中。考虑到可能有环的情况,因此需要用valid和mark的额外标记位来判断。
class Solution {
List<Integer>[] edges;
// 0 not visited, 1 visiting for circle detect, 2 visited
int[] mark;
// has circle
boolean valid = true;
int res[];
int resIndex = 0;
public int[] findOrder(int numCourses, int[][] prerequisites) {
// read data
edges = new List[numCourses];
res = new int[numCourses];
mark = new int[numCourses];
// fill empty to lists
for(int i = 0; i < numCourses; i++){
edges[i] = new ArrayList<>();
}
// read edges
for(int[] edge : prerequisites){
int to = edge[1];
int from = edge[0];
List<Integer> l = edges[from];
l.add(to);
}
for(int i = 0; i < numCourses; i++){
if(mark[i] == 0){
dfs(i);
if(!valid){
return new int[0];
}
}
}
return res;
}
public void dfs(int i){
if(mark[i] == 2){
return;
}
mark[i] = 1;
List<Integer> tos = edges[i];
for(int to : tos){
if(mark[to] == 1){
valid = false;
return;
}
dfs(to);
if(!valid){
return;
}
}
mark[i] = 2;
res[resIndex++] = i;
}
}
Kahn(BFS)
Kahn算法的思路是,先将左右的没有入边的节点加入队列,每次从队列中取出一个没有入边的节点,删除这个节点及其所有出边,并放入结果中。然后遍历其所有出边的对应节点(此时这些节点的入边数量就少1),把所有没有入边的节点放入队列中。这就重复了直到所有点都放入结果中。
在图中有环时,会有节点永远不会被加入到队列中,因此算法结束后结果集数量少于节点数量。
class Solution {
int[] indegree;
// nodes which indegree is zero
Queue<Integer> setOfZeroIndegree;
List<Integer>[] edges;
// result set
int[] res;
int resIndex = 0;
public int[] findOrder(int numCourses, int[][] prerequisites) {
// init
indegree = new int[numCourses];
res = new int[numCourses];
setOfZeroIndegree = new LinkedList<>();
edges = new List[numCourses];
for(int i = 0; i < numCourses; i++){
edges[i] = new ArrayList<>();
}
for(int[] edge : prerequisites){
int to = edge[0];
int from = edge[1];
edges[from].add(to);
indegree[to]++;
}
for(int i = 0; i < numCourses; i++){
if(indegree[i] == 0){
setOfZeroIndegree.add(i);
}
}
int cur;
while(!setOfZeroIndegree.isEmpty()){
cur = setOfZeroIndegree.poll();
res[resIndex++] = cur;
// for all to nodes, indegree sub one. if nodes has no indegree, put into setOfZeroIndegree.
for(int i : edges[cur]){
indegree[i]--;
if(indegree[i] == 0){
setOfZeroIndegree.add(i);
}
}
}
// graph has circle
if(resIndex != numCourses){
return new int[0];
}
return res;
}
}