拓扑排序
概念
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
拓扑排序主要用来解决有向图中的依赖解析(dependency resolution)问题。
举例来说,如果我们将一系列需要运行的任务构成一个有向图,图中的有向边则代表某一任务必须在另一个任务之前完成这一限制。那么运用拓扑排序,我们就能得到满足执行顺序限制条件的一系列任务所需执行的先后顺序。当然也有可能图中并不存在这样一个拓扑顺序,这种情况下我们无法根据给定要求完成这一系列任务,这种情况称为循环依赖(circular dependency)。
例1:如下图所示为一个有向图:
可以得到两个不同的拓扑排序结果:[1, 2, 3, 4, 5]
和[1, 2, 3, 5, 4]
。
拓扑排序存在的前提:
当且仅当一个有向图为有向无环图(directed acyclic graph,或称DAG)时,才能得到对应于该图的拓扑排序。每一个有向无环图都至少存在一种拓扑排序。该论断可以利用反证法证明如下:
假设我们有一由
到
这n个结点构成的有向图,且图中
这些结点构成一个环。这即是说对于所有
1≤i<n-1
,图中存在一条有向边从指向
。同时还存在一条从
指向
的边。假设该图存在一个拓扑排序。
那么基于这样一个有向图,显然我们可以得知对于所有1≤i<n-1,必须在之前被遍历,也就是必须在之前被遍历。同时由于还存在一条从指向的边,必须在之前被遍历。这里出现了与我们的假设所冲突的结果。因此我们可以知道,该图存在拓扑排序的假设不成立。也就是说,对于非有向无环图而言,其拓扑排序不存在。
实现方式
bfs广度遍历
与普通的广度优先遍历唯一的区别在于需要维护每一个节点对应的入度,并在遍历的每一层时选取入度为0的节点开始遍历(而普通的广度优先遍历则无此限制,可以从每一层任意一个节点开始遍历)。这个算法描述如下:
- 统计图的每一个节点的入度存储与数组inDegree。
- 选取入度为0的节点加入队列
- 从队列中取出一个节点,
(a) 将该节点加入输出
(b) 将该节点的所有邻接点的入度树减1,减1后入度数变为0的节点加入队列 - 重复步骤3,直到遍历完所有的结点。
- 如果无法遍历完所有的结点,则意味着当前的图不是有向无环图。不存在拓扑排序。
**由于只有入度为0的节点才会被放入队列,当存在环时,环上的节点将不会放入队列,因此不会出现在最终的拓扑排序中。**事实上,在基于广度优先搜索的拓扑排序中,可以根据最终拓扑排序输出列表的长度是否等于图的节点数,来判断输入图是否存在拓扑排序。
时间复杂度: ,其中
n
为图中的结点数目,e
为图中的边的数目
空间复杂度: ,需要存储成邻接表,空间复杂度为
,
leetcode题:
import java.util.LinkedList;
import java.util.Queue;/*** Description:leetcode-207. 课程表** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/3*/
public class Main207 {/**** 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。* 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,* 表示如果要学习课程 ai 则 必须 先学习课程 bi 。** 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。* 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。** 输入:numCourses = 2, prerequisites = [[1,0]]* 输出:true* 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。** [[0,2],[1,2],[2,0]] false**//*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点的入度*/int[] inDegree;/*** bfs实现* @param numCourses 节点数* @param prerequisites 依赖关系* @return*/public boolean canFinish(int numCourses, int[][] prerequisites) {// 初始化graph = new LinkedList[numCourses];inDegree = new int[numCourses];for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);inDegree[x] ++;}// 入度0添加Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; ++i) {if (inDegree[i] == 0) {queue.offer(i);}}// 拓扑排序遍历int visited = 0;while (!queue.isEmpty()){visited ++;Integer point = queue.poll();// 指向的节点入度 - 1LinkedList<Integer> edge = graph[point];for (Integer nextPoint : edge) {inDegree[nextPoint] --;// 入度0添加到队列if (inDegree[nextPoint] == 0){queue.offer(nextPoint);}}}return visited == numCourses;}public static void main(String[] args) {Main207 main207 = new Main207();int[][] arr = {{1,0}, {1,2}, {0, 1}};System.out.println(main207.canFinish(3, arr));}
}
import java.util.LinkedList;
import java.util.Queue;/*** Description:leetcode-210. 课程表 II** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/4*/
public class Main210 {/*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点的入度*/int[] inDegree;/*** bfs* @param numCourses 节点数* @param prerequisites 边,依赖关系* @return*/public int[] findOrder(int numCourses, int[][] prerequisites) {int[] result = new int[numCourses];// 初始化graph = new LinkedList[numCourses];inDegree = new int[numCourses];for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);inDegree[x] ++;}// 入度0添加Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; ++i) {if (inDegree[i] == 0) {queue.offer(i);}}// 拓扑排序遍历int visited = 0;while (!queue.isEmpty()){Integer point = queue.poll();// 指向的节点入度 - 1LinkedList<Integer> edge = graph[point];for (Integer nextPoint : edge) {// 入度0添加到队列if (--inDegree[nextPoint] == 0){queue.offer(nextPoint);}}result[visited ++] = point;}if (visited != numCourses){return new int[0];}return result;}public static void main(String[] args) {Main210 main210 = new Main210();int[][] arr = {{1,0}};System.out.println(main210.findOrder( 2, arr));}
}
dfs深度遍历
使用深度优先搜索实现拓扑排序的基本思想是:对于一个特定节点,如果该节点的所有相邻节点都已经搜索完成,则该节点也会变成已经搜索完成的节点,在拓扑排序中,该节点位于其所有相邻节点的前面。一个节点的相邻节点指的是从该节点出发通过一条有向边可以到达的节点。
由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:
- 0:未访问;
- 1:访问中;
- 2:已访问;
初始时,所有节点的状态都是「未访问」。
每一轮搜索时,任意选取一个「未访问」的节点 u,从节点 u 开始深度优先搜索。将节点 u 的状态更新为「访问中」,对于每个与节点 u 相邻的节点 v,判断节点 v 的状态,执行如下操作:
- 如果节点 v 的状态是「未访问」,则继续搜索节点 v;
- 如果节点 v 的状态是「访问中」,则找到有向图中的环,因此不存在拓扑排序;
- 如果节点 v 的状态是「已访问」,则节点 v 已经搜索完成并加入输出排序列表,节点 u 尚未完成搜索,因此节点 u 的拓扑顺序一定在节点 v 的前面,不需要执行任何操作。
- 当节点 u 的所有相邻节点的状态都是「已访问」时,将节点 u 的状态更新为「已访问」,并将节点 u 加入输出排序列表。
- 当所有节点都访问结束之后,如果没有找到有向图中的环,则存在拓扑排序,所有节点从栈顶到栈底的顺序即为拓扑排序。
时间复杂度: ,其中
n
为图中的结点数目,e
为图中的边的数目
空间复杂度: ,对图进行深度优先搜索,我们需要存储成邻接表的形式,空间复杂度为
,深度优先搜索的过程中,我们需要最多 O(n)的栈空间(递归)进行深度优先搜索,因此总空间复杂度为
leetcode题:
import java.util.LinkedList;
import java.util.Queue;/*** Description:leetcode-207. 课程表** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/3*/
public class Main207 {/**** 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。* 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,* 表示如果要学习课程 ai 则 必须 先学习课程 bi 。** 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。* 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。** 输入:numCourses = 2, prerequisites = [[1,0]]* 输出:true* 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。** [[0,2],[1,2],[2,0]] false**//*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点访问状态, 0未访问 1访问中 2已访问*/int[] visited;/*** 是否存在环*/boolean isCircle = false;/*** dfs实现* @param numCourses 节点数* @param prerequisites 依赖关系* @return*/public boolean canFinish(int numCourses, int[][] prerequisites) {// 初始化graph = new LinkedList[numCourses];visited = new int[numCourses];for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();visited[i] = 0;}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);}// 未访问的节点一直递归for (int i = 0; i < numCourses && !isCircle; i++) {if (visited[i] == 0) {dfs(i);}}return !isCircle;}private void dfs(int point){visited[point] = 1;// 递归指向的相邻节点for (int nextPoint: graph[point]) {if (visited[nextPoint] == 0) {// 未访问则继续向下递归dfs(nextPoint);if (isCircle) {return;}} else if (visited[nextPoint] == 1) {// 出现环isCircle = true;return;}}// 已访问visited[point] = 2;}public static void main(String[] args) {Main207 main207 = new Main207();int[][] arr = {{1,0}, {1,2}, {0, 1}};System.out.println(main207.canFinish( 3, arr));}
}
import java.util.LinkedList;/*** Description:leetcode-210. 课程表 II** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/4*/
public class Main210 {/*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点访问状态, 0未访问 1访问中 2已访问*/int[] visited;/*** 是否存在环*/boolean isCircle = false;/*** 拓扑排序集*/int[] result;/*** 拓扑排序集下标*/int index;/*** dfs* @param numCourses 节点数* @param prerequisites 边,依赖关系* @return*/public int[] findOrder(int numCourses, int[][] prerequisites) {result = new int[numCourses];// 初始化graph = new LinkedList[numCourses];visited = new int[numCourses];index = numCourses - 1;for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);}// 未访问的节点一直递归for (int i = 0; i < numCourses && !isCircle; i++) {if (visited[i] == 0){dfs(i);}}if (isCircle){return new int[0];}return result;}private void dfs(int point){visited[point] = 1;// 递归指向的相邻节点for (Integer nextPoint : graph[point]) {if (visited[nextPoint] == 0){// 未访问则继续向下递归dfs(nextPoint);if (isCircle) {return;}continue;}if(visited[nextPoint] == 1){// 出现环isCircle = true;return;}}// 已访问,由于被依赖的节点先遍历,记录在尾部result[index --] = point;visited[point] = 2;}public static void main(String[] args) {Main210 main210 = new Main210();int[][] arr = {{1,0}};System.out.println(main210.findOrder( 2, arr));}
}