今天学习拓扑排序。如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(Directed Acyclic Graph,DAG)。拓扑排序就是将有向无环图的所有顶点排序,使得图中任意两个点 u、v,只要存在边 u → v,那么拓扑排序中 u 一定在 v 前面。例如,高等数学、线性代数都可以直接开始学习,复变函数要学习完前两门课才能学习,所以,拓扑排序可以是:高等数学→线性代数→复变函数,或者线性代数→高等数学→复变函数。下面讲解具体算法的思路。
1.拓扑排序算法
基本思想如下:
- 定义一个队列 q,把所有入度为 0 的节点入队;
- 取队首节点,输出。删除它的出边,令出边到达的顶点入度减 1,如果该顶点入度变成 0,则将其加入队列;
- 反复进行第 2 步,直到队空。如果所有点都被访问,则 G 是有向无环图,否则有环。
代码如下:
List<List<Integer>> edges = new ArrayList<>(); // 邻接表
int[] inDegree = new int[n]; // 记录每个点的入度public int[] topologicSort(int n, List<List<Integer>> edges, int[] inDegree) {int[] ret = new int[n];int num = 0; // 记录加入拓扑排序的定点数Queue<Integer> q = new LinkedList<>();for (int i = 0; i < n; i++) { // 入度为0的节点入队if (inDegree[i] == 0) q.offer(i);}while (!q.isEmpty()) {int u = q.poll();ret[num] = u;for (int v : edges.get(u)) { // 取出后继节点inDegree[v]--;if (inDegree[v] == 0) {q.offer(v);}}edges.get(u).clear(); // 删除顶点u的所有出边num++; // 加入拓扑排序的定点数加1}if (num != n) return new int[0]; // 排序失败else return ret; // 排序成功
}
2.练习:力扣207. 课程表[1]
你这个学期必须选修 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 。这是不可能的。
该题只需判断能否完成拓扑排序,即有没有环,解法如下:
class Solution {private List<List<Integer>> edges;private int[] inDegree;public boolean canFinish(int numCourses, int[][] prerequisites) {edges = new ArrayList<>(); // 邻接表for (int i = 0; i < numCourses; i++) {edges.add(new ArrayList<>());}inDegree = new int[numCourses]; // 记录入度// 建图for (int[] prerequisite : prerequisites) {inDegree[prerequisite[0]]++;edges.get(prerequisite[1]).add(prerequisite[0]);}return topologicSort(numCourses);}private boolean topologicSort(int n) {int cnt = 0; // 记录参加过拓扑排序的节点数量Queue<Integer> q = new LinkedList<>();for (int i = 0; i < n; i++) {if (inDegree[i] == 0) {q.offer(i);}}while (!q.isEmpty()) {int cur = q.poll();cnt++;for (int next : edges.get(cur)) {inDegree[next]--;if (inDegree[next] == 0) {q.offer(next);}}}return cnt == n;}
}
3.练习:210. 课程表 II[2]
现在你总共有 numCourses
门课需要选,记为 0
到 numCourses - 1
。给你一个数组 prerequisites
,其中 prerequisites[i] = [ai, bi]
,表示在选修课程 ai
前 必须 先选修 bi
。
- 例如,想要学习课程
0
,你需要先完成课程1
,我们用一个匹配来表示:[0,1]
。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: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] 。
示例 3:
输入:numCourses = 1, prerequisites = []
输出:[0]
典型的拓扑排序题,在上一题基础上记录排序的结果即可:
class Solution {public int[] findOrder(int numsCourses, int[][] prerequisites) {List<List<Integer>> edges = new ArrayList<>(); // 邻接表for (int i = 0; i < numsCourses; i++) {edges.add(new ArrayList<>());}int[] inDegree = new int[numsCourses]; // 记录每个点的入度for (int[] prerequisite : prerequisites) {edges.get(prerequisite[1]).add(prerequisite[0]);inDegree[prerequisite[0]]++; // 注意是入度}return topologicSort(numsCourses, edges, inDegree);}public int[] topologicSort(int n, List<List<Integer>> edges, int[] inDegree) {int[] ret = new int[n];int num = 0; // 记录加入拓扑排序的定点数Queue<Integer> q = new LinkedList<>();for (int i = 0; i < n; i++) { // 入度为0的节点入队if (inDegree[i] == 0) q.offer(i);}while (!q.isEmpty()) {int u = q.poll();ret[num] = u;for (int v : edges.get(u)) { // 取出后继节点inDegree[v]--;if (inDegree[v] == 0) {q.offer(v);}}edges.get(u).clear(); // 删除顶点u的所有出边num++; // 加入拓扑排序的定点数加1}if (num != n) return new int[0];else return ret;}
}
Reference
[1]力扣207. 课程表:https://leetcode-cn.com/problems/course-schedule/
[2]力扣210. 课程表 IIhttps://leetcode-cn.com/problems/course-schedule-ii/
欢迎关注。每天分享大数据开发面经和技术。