diff --git a/notes/Leetcode 题解 - 图.md b/notes/Leetcode 题解 - 图.md index cbd6dbbd..ace24f8d 100644 --- a/notes/Leetcode 题解 - 图.md +++ b/notes/Leetcode 题解 - 图.md @@ -8,6 +8,7 @@ * [2. 课程安排的顺序](#2-课程安排的顺序) * [并查集](#并查集) * [1. 冗余连接](#1-冗余连接) + * [参考资料](#参考资料) @@ -46,6 +47,8 @@ The graph looks like this: We cannot find a way to divide the set of nodes into two independent subsets. ``` +从一个未着色的节点开始 DFS,给它涂色 0,它的邻居涂色 1,邻居的邻居再涂色 0。如果遇到一个已经着色的节点,颜色与应该涂的颜色不同,就说明有两个相邻节点必须同色,不是二分图。例 2 中 0 涂色 0、1 涂色 1、2 涂色 0,而 2 与 0 相邻,矛盾。 + ```java public boolean isBipartite(int[][] graph) { int[] colors = new int[graph.length]; @@ -74,7 +77,7 @@ private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] gra ## 拓扑排序 -常用于在具有先序关系的任务规划中。 +拓扑排序指的是把有向无环图的节点排成一个序列,使得每条边 u → v 中 u 都排在 v 的前面,常用于有先后依赖的任务规划,例如安排课程的学习顺序。图中有环时不存在拓扑排序。 ### 1. 课程安排的合法性 @@ -96,6 +99,8 @@ return false 本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。 +用 DFS 检测环时,节点有三种状态:没访问过;在当前的递归路径上(localMarked 为 true);已经访问完并且确认从它出发没有环(只有 globalMarked 为 true)。沿着边走到一个仍在当前路径上的节点,说明绕回了自己,存在环。只用一个 visited 数组无法区分后两种状态,例如 0 → 1、0 → 2、2 → 1 中,从 2 走到 1 时 1 已经访问过,但没有环。 + ```java public boolean canFinish(int numCourses, int[][] prerequisites) { List[] graphic = new List[numCourses]; @@ -147,9 +152,9 @@ private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3]. ``` -使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。 +使用 DFS 来实现拓扑排序:在一个节点的所有后继都访问完之后,再把它放入后序遍历的结果,也就是 postOrder.push(curNode) 所在的位置。 -证明:对于任何先序关系:v-\>w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。 +对于任何一条边 v-\>w,后序遍历可以保证 w 先于 v 进入结果,因此后序遍历结果的逆序就是拓扑排序。代码中的边从课程指向它的先修课程(graphic[pre[0]].add(pre[1])),先修课程会先进入后序结果,所以按入栈的顺序输出就是可行的上课顺序,不需要再逆序。以输入为例,从 0 出发时 0 没有先修课程,0 先入栈;从 1 出发时先修课程 0 已访问,1 入栈;随后是 2、3,结果为 [0,1,2,3]。 ```java public int[] findOrder(int numCourses, int[][] prerequisites) { @@ -197,6 +202,42 @@ private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List
+ +```java +public int[] findOrder(int numCourses, int[][] prerequisites) { + List[] graphic = new List[numCourses]; + for (int i = 0; i < numCourses; i++) { + graphic[i] = new ArrayList<>(); + } + int[] inDegree = new int[numCourses]; + for (int[] pre : prerequisites) { + graphic[pre[1]].add(pre[0]); // 边从先修课程指向后续课程 + inDegree[pre[0]]++; + } + Queue queue = new LinkedList<>(); + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) { + queue.add(i); + } + } + int[] orders = new int[numCourses]; + int cnt = 0; + while (!queue.isEmpty()) { + int cur = queue.poll(); + orders[cnt++] = cur; + for (int next : graphic[cur]) { + if (--inDegree[next] == 0) { + queue.add(next); + } + } + } + return cnt == numCourses ? orders : new int[0]; +} +``` + ## 并查集 并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 @@ -218,6 +259,8 @@ Explanation: The given undirected graph will be like this: 题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。 +按顺序加边,用并查集记录哪些节点已经连通。如果一条边的两个端点在加它之前就已经连通,这条边就形成了环,是多余的边。图中只有一个环,环上最后加入的边就是第一次检测到的这条边,满足题目“有多个答案时返回最后出现的边”的要求。这里的并查集是最简单的 Quick Find 实现,更快的实现见[算法 - 并查集](算法%20-%20并查集.md)。 + ```java public int[] findRedundantConnection(int[][] edges) { int N = edges.length; @@ -265,3 +308,8 @@ private class UF { } } ``` + +## 参考资料 + +- [LeetCode 题库](https://leetcode.com/problemset/) +- Sedgewick R, Wayne K. 算法(第 4 版)[M]. 人民邮电出版社, 2012. diff --git a/notes/pics/lc-graph-topo-kahn.png b/notes/pics/lc-graph-topo-kahn.png new file mode 100644 index 00000000..6ccbebfd Binary files /dev/null and b/notes/pics/lc-graph-topo-kahn.png differ