携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情
一、前言
拓扑序: 如果图中从 V
到 W
有一条有向路径,则 V
一定排在 W
之前。满足此条件的顶点列称为一个拓扑序。
拓扑排序: 获得一个拓扑序的过程就是拓扑排序。
AOV
(Activity On Vertex
)网络: 如果有合理的拓扑序,则必定是有向无环图(Directed Acyclic Graph,DAG
)。
容易想到暴力法:时间复杂度 T= O(|V|^2)
// 伪代码如下:
void topSort() {
for (int cnt = 0; cnt < |V|; ++cnt) {
V = 未输入的入度为 0 的顶点 // O(|V|)
if (这样的 V 不存在) {
Error("图中有回路");
break;
}
输出 v, 或者记录 v 的输出序号
for ( v 的每个邻接点 w) {
Indegree[w] --; // 入度 - 1
}
}
}
聪明的算法:随时将入度为 0 的顶点放到一个容器里
时间复杂度
T= O(|V| + |E|)
// 用来检测有向图是否 DAG
void topSort() {
for (图中每个顶点 v ) {
if (Indegree[v] == 0) Enqueue(v, Q); // 放入容器
}
while (!isEmpty(Q)) {
v = Dequeue(Q);
// 输出 v,或者记录v 的输出序号
++cnt; // 统计节点
for (v 的每个邻接点 w) {
if (--Indegree[w] == 0) {
Enqueue(w, Q); // 放入容器
}
}
}
if (cnt != |V|) { // 统计节点数 是否等于图中所有节点数
Error("图中有回路");
}
}
二、题目
(1)课程安排
题目描述
题目说的是:你有 n 门课要上,课程编号从 0 到 n-1。
在上某些课之前你需要先上另外一些课程,这种依赖关系可以用一个数对来表示。 比如 (0,1) 数对表示在上课程 0 之前,你需要先上课程 1。现在给你 n 门课以及它们之间的依赖关系,你要判断是否有可能上完所有课程。
比如说,给你的课程数量 n 等于 5:
n = 5
课程之间的依赖关系是:
(1, 0)
(3, 0)
(3, 1)
(2, 1)
(2, 3)
(4, 2)
(4, 3)
我们只要按照 0, 1, 3, 2, 4 的顺序来排课,就可以在满足依赖关系的条件下完成这 5 门课。因此返回 true。
提示:上述依赖关系可以转成以下有向图
0 ---> 3 ---> 4
\ ^ \ ^
\ / \ /
v/ v/
1 ---> 2
思路解法
此题意:判断有向图里是否有环
习惯用法:节点总数是
V
,边的总数是E
此题有两种解法:
- 方法一:
DFS
+ 回溯:每个节点遍历一次 - 方法二:拓扑排序
方法一:DFS
+ 回溯
这个比较好理解,看图吧。
- 构建邻接表
- 遍历每个节点
public class LeetCode_207 {
// 方法一: dfs + 回溯
// Time: O(V + E), Space: O(V + E), Faster: 64.27%
public boolean canFinishDFS(int numCourses, int[][] prerequisites) {
if (numCourses <= 1 || prerequisites == null) return true;
// 构建图: 构建邻接表
List<List<Integer>> graph = new ArrayList<>(numCourses);
for (int i = 0; i < numCourses; ++i) {
graph.add(new LinkedList<>());
}
for (int[] pair : prerequisites) {
graph.get(pair[1]).add(pair[0]);
}
// 记录这个节点之后是否有环, true 无环 false 可能有环
boolean[] checked = new boolean[numCourses];
// 标记是否访问过
boolean[] visited = new boolean[numCourses];
for (int i = 0; i < numCourses; ++i)
if (!checked[i] && hasCycle(graph, visited, checked, i))
return false;
return true;
}
private boolean hasCycle(List<List<Integer>> graph, boolean[] visited,
boolean[] checked, int v) {
if (visited[v]) return true;
visited[v] = true;
for (int i : graph.get(v)) {
if (!checked[i] && hasCycle(graph, visited, checked, i))
return true;
}
checked[v] = true;
visited[v] = false;
return false;
}
}
方法二:拓扑排序
- 构建邻接表
- 记录每个节点的入度
- 遍历:计算入度为0的节点,将其出度减去
- 是否有环:即入度为0的节点数 == 总节点数
AC
代码:
public class LeetCode_207 {
// 方法二: 拓扑排序
// Time: O(V + E), Space: O(V + E), Faster: 39.75%
public boolean canFinishTopSortAdjList(int numCourses, int[][] prerequisites) {
if (numCourses <= 1 || prerequisites == null || prerequisites.length == 0)
return true;
// 记录每个节点的入度数
int[] inDegree = new int[numCourses];
// 构建邻接表
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; ++i) {
graph.add(new LinkedList<>());
}
// 建立节点的关系
for (int[] pair : prerequisites) {
graph.get(pair[1]).add(pair[0]);
++inDegree[pair[0]];
}
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < inDegree.length; ++i) {
if (inDegree[i] == 0) {
q.add(i);
}
}
int count = 0;
while (!q.isEmpty()) {
int v = q.poll();
++count;
for (int i : graph.get(v)) {
--inDegree[i];
if (inDegree[i] == 0) q.add(i);
}
}
return count == numCourses;
}
}