|
10 | 10 |
|
11 | 11 | ## 2. 拓扑排序的实现方法
|
12 | 12 |
|
13 |
| -拓扑排序有两种实现方法,分别是 Kahn 算法和 DFS 深度优先搜索算法。接下来我们依次来看下它们是如何实现的。 |
| 13 | +拓扑排序有两种实现方法,分别是「Kahn 算法」和「DFS 深度优先搜索算法」。接下来我们依次来看下它们是如何实现的。 |
14 | 14 |
|
15 | 15 | ### 2.1 Kahn 算法
|
16 | 16 |
|
| 17 | +> **Kahn 算法的基本思想**: |
| 18 | +
|
17 | 19 | #### 2.1.1 Kahn 算法的实现步骤
|
18 | 20 |
|
19 | 21 | 1. 使用数组 $indegrees$ 用于记录图中各个顶点的入度。
|
20 | 22 | 2. 维护一个入度为 $0$ 的顶点集合 $S$(可使用栈、队列、优先队列)。
|
21 | 23 | 3. 每次从集合中选择任何一个没有前驱(即入度为 $0$)的顶点 $u$,将其输出到拓扑序列 $order$ 中。
|
22 | 24 | 4. 从图中删除该顶点 $u$,并且删除从该顶点出发的有向边 $<u, v>$(也就是把该顶点可达的顶点入度都减 $1$)。如果删除该边后顶点 $v$ 的入度变为 $0$,则将顶点 $v$ 放入集合 $S$ 中。
|
23 |
| -5. 重复上述过程,直到集合 $S$ 为空。 |
24 |
| -6. 检测图中是否存在任何边,如果有,则该图一定存在环路。否则 $order$ 中顶点的顺序就是拓扑排序的结果。 |
| 25 | +5. 重复上述过程,直到集合 $S$ 为空,或者图中还有顶点未被访问(说明一定存在环路,无法形成拓扑序列)。 |
| 26 | +6. 如果不存在环路,则 $order$ 中顶点的顺序就是拓扑排序的结果。 |
25 | 27 |
|
26 | 28 | #### 2.1.2 Kahn 算法的实现代码
|
27 | 29 |
|
28 | 30 | ```Python
|
| 31 | +import collections |
29 | 32 |
|
| 33 | +class Solution: |
| 34 | + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) |
| 35 | + def topologicalSortingKahn(self, graph: dict): |
| 36 | + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 |
| 37 | + for u in graph: |
| 38 | + for v in graph[u]: |
| 39 | + indegrees[v] += 1 # 统计所有顶点入度 |
| 40 | + |
| 41 | + # 将入度为 0 的顶点存入集合 S 中 |
| 42 | + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) |
| 43 | + order = [] # order 用于存储拓扑序列 |
| 44 | + |
| 45 | + while S: |
| 46 | + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 |
| 47 | + order.append(u) # 将其输出到拓扑序列 order 中 |
| 48 | + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v |
| 49 | + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 |
| 50 | + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 |
| 51 | + S.append(v) # 将其放入集合 S 中 |
| 52 | + |
| 53 | + if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 |
| 54 | + return [] |
| 55 | + return order # 返回拓扑序列 |
| 56 | + |
| 57 | + |
| 58 | + def findOrder(self, n: int, edges): |
| 59 | + # 构建图 |
| 60 | + graph = dict() |
| 61 | + for i in range(n): |
| 62 | + graph[i] = [] |
| 63 | + |
| 64 | + for u, v in edges: |
| 65 | + graph[u].append(v) |
| 66 | + |
| 67 | + return self.topologicalSortingKahn(graph) |
30 | 68 | ```
|
31 | 69 |
|
32 |
| -### 2.2 DFS 深度优先搜索算法 |
| 70 | +### 2.2 基于 DFS 实现拓扑排序算法 |
| 71 | + |
| 72 | +> **基于 DFS 实现拓扑排序算法的基本思想**: |
| 73 | +> |
| 74 | +> 1. 对于一个顶点 $u$,深度游先生遍历从该顶点出发的有向边 $<u, v>$。如果从该顶点 $u$ 出发的所有相邻顶点 $v$ 都已经搜索完毕,则在搜索回溯到顶点 $u$ 时,$u$ 本身也会编程一个已经搜索完的顶点。 |
| 75 | +> 2. 在拓扑排序的序列中,该顶点 $u$ 位于其所有相邻顶点 $v$ 的前面。 |
| 76 | +> 3. 这样一来,我们对每个顶点进行回溯时,将其放入栈中,则最终从栈顶到栈底的序列就是一种拓扑排序。 |
33 | 77 |
|
34 |
| -#### 2.2.1 DFS 深度优先搜索算法实现步骤 |
| 78 | +#### 2.2.1 基于 DFS 实现拓扑排序算法实现步骤 |
35 | 79 |
|
36 |
| -1. 以任意顺序循环遍历图中的每个顶点,将其输出到拓扑序列中。 |
37 |
| -2. 如果搜索时遇到之前已经遇到的顶点,或者碰到叶节点,则中止算法。 |
| 80 | +1. 使用集合 $visited$ 用于记录当前顶点是否被访问过,避免重复访问。 |
| 81 | +2. 使用集合 $onStack$ 用于记录同一次深度优先搜索时,当前顶点是否被访问过。如果当前顶点被访问过,则说明图中存在环路,无法构成拓扑序列。 |
| 82 | +3. 使用布尔变量 $hasCycle$ 用于判断图中是否存在环。 |
| 83 | +4. 从任意一个未被访问的顶点 $u$ 出发。 |
| 84 | + 1. 如果顶点 $u$ 在同一次深度优先搜索时被访问过,则说明存在环。 |
| 85 | + 2. 如果当前顶点被访问或者有环时,则无需再继续遍历,直接返回。 |
| 86 | + |
| 87 | +5. 将顶点 $u$ 标记为被访问过,并在本次深度优先搜索中标记为访问过。然后深度游先生遍历从顶点 $u$ 出发的有向边 $<u, v>$。 |
| 88 | +6. 当顶点 $u$ 的所有相邻顶点 $v$ 都被访问后,回溯前记录当前节点 $u$(将当前节点 $u$ 输出到拓扑序列 $order$ 中)。 |
| 89 | +7. 取消本次深度优先搜索时,顶点 $u$ 的访问标记。 |
| 90 | +8. 对其他未被访问的顶点重复 $4 \sim 7$ 步过程,直到所有节点都遍历完,或者出现环。 |
| 91 | +9. 如果不存在环路,则将 $order$ 逆序排序后,顶点的顺序就是拓扑排序的结果。 |
38 | 92 |
|
39 | 93 | #### 2.2.2 DFS 深度优先搜索算法实现代码
|
40 | 94 |
|
41 | 95 | ```Python
|
| 96 | +import collections |
42 | 97 |
|
| 98 | +class Solution: |
| 99 | + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) |
| 100 | + def topologicalSortingDFS(self, graph: dict): |
| 101 | + visited = set() # 记录当前顶点是否被访问过 |
| 102 | + onStack = set() # 记录同一次深搜时,当前顶点是否被访问过 |
| 103 | + order = [] # 用于存储拓扑序列 |
| 104 | + hasCycle = False # 用于判断是否存在环 |
| 105 | + |
| 106 | + def dfs(u): |
| 107 | + nonlocal hasCycle |
| 108 | + if u in onStack: # 同一次深度优先搜索时,当前顶点被访问过,说明存在环 |
| 109 | + hasCycle = True |
| 110 | + if u in visited or hasCycle: # 当前节点被访问或者有环时直接返回 |
| 111 | + return |
| 112 | + |
| 113 | + visited.add(u) # 标记节点被访问 |
| 114 | + onStack.add(u) # 标记本次深搜时,当前顶点被访问 |
| 115 | + |
| 116 | + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v |
| 117 | + dfs(v) # 递归访问节点 v |
| 118 | + |
| 119 | + order.append(u) # 后序遍历顺序访问节点 u |
| 120 | + onStack.remove(u) # 取消本次深搜时的 顶点访问标记 |
| 121 | + |
| 122 | + for u in graph: |
| 123 | + if u not in visited: |
| 124 | + dfs(u) # 递归遍历未访问节点 u |
| 125 | + |
| 126 | + if hasCycle: # 判断是否存在环 |
| 127 | + return [] # 存在环,无法构成拓扑序列 |
| 128 | + order.reverse() # 将后序遍历转为拓扑排序顺序 |
| 129 | + return order # 返回拓扑序列 |
| 130 | + |
| 131 | + def findOrder(self, n: int, edges): |
| 132 | + # 构建图 |
| 133 | + graph = dict() |
| 134 | + for i in range(n): |
| 135 | + graph[i] = [] |
| 136 | + for v, u in edges: |
| 137 | + graph[u].append(v) |
| 138 | + |
| 139 | + return self.topologicalSortingDFS(graph) |
43 | 140 | ```
|
44 | 141 |
|
45 | 142 | ## 3. 拓扑排序的应用
|
46 | 143 |
|
47 | 144 | 拓扑排序可以用来解决一些依赖关系的问题,比如项目的执行顺序,课程的选修顺序等。
|
48 | 145 |
|
| 146 | +### 3.1 课程表 II |
| 147 | + |
| 148 | +#### 3.1.1 题目链接 |
| 149 | + |
| 150 | +- [210. 课程表 II - 力扣](https://leetcode.cn/problems/course-schedule-ii/) |
| 151 | + |
| 152 | +#### 3.1.2 题目大意 |
| 153 | + |
| 154 | +**描述**:给定一个整数 $numCourses$,代表这学期必须选修的课程数量,课程编号为 $0 \sim numCourses - 1$。再给定一个数组 $prerequisites$ 表示先修课程关系,其中 $prerequisites[i] = [ai, bi]$ 表示如果要学习课程 $ai$ 则必须要学习课程 $bi$。 |
| 155 | + |
| 156 | +**要求**:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 |
| 157 | + |
| 158 | +**说明**: |
| 159 | + |
| 160 | +- $1 \le numCourses \le 2000$。 |
| 161 | +- $0 \le prerequisites.length \le numCourses \times (numCourses - 1)$。 |
| 162 | +- $prerequisites[i].length == 2$。 |
| 163 | +- $0 \le ai, bi < numCourses$。 |
| 164 | +- $ai \ne bi$。 |
| 165 | +- 所有$[ai, bi]$ 互不相同。 |
| 166 | + |
| 167 | +**示例**: |
| 168 | + |
| 169 | +- 示例 1: |
| 170 | + |
| 171 | +```Python |
| 172 | +输入:numCourses = 2, prerequisites = [[1,0]] |
| 173 | +输出:[0,1] |
| 174 | +解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1]。 |
| 175 | +``` |
| 176 | + |
| 177 | +- 示例 2: |
| 178 | + |
| 179 | +```Python |
| 180 | +输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] |
| 181 | +输出:[0,2,1,3] |
| 182 | +解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 |
| 183 | +因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3]。 |
| 184 | +``` |
| 185 | + |
| 186 | +#### 3.1.3 解题思路 |
| 187 | + |
| 188 | +##### 思路 1:拓扑排序 |
| 189 | + |
| 190 | +这道题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的升级版,只需要在上一题的基础上增加一个答案数组 $order$ 即可。 |
| 191 | + |
| 192 | +1. 使用哈希表 $graph$ 存放课程关系图,并统计每门课程节点的入度,存入入度列表 $indegrees$。 |
| 193 | +2. 借助队列 $S$,将所有入度为 $0$ 的节点入队。 |
| 194 | +3. 从队列中选择一个节点 $u$,并将其加入到答案数组 $order$ 中。 |
| 195 | +4. 从图中删除该顶点 $u$,并且删除从该顶点出发的有向边 $<u, v>$(也就是把该顶点可达的顶点入度都减 $1$)。如果删除该边后顶点 $v$ 的入度变为 $0$,则将其加入队列 $S$ 中。 |
| 196 | +5. 重复上述步骤 $3 \sim 4$,直到队列中没有节点。 |
| 197 | +6. 最后判断总的顶点数和拓扑序列中的顶点数是否相等,如果相等,则返回答案数组 $order$,否则,返回空数组。 |
| 198 | + |
| 199 | +##### 思路 1:代码 |
| 200 | + |
| 201 | +```Python |
| 202 | +import collections |
| 203 | + |
| 204 | +class Solution: |
| 205 | + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) |
| 206 | + def topologicalSortingKahn(self, graph: dict): |
| 207 | + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 |
| 208 | + for u in graph: |
| 209 | + for v in graph[u]: |
| 210 | + indegrees[v] += 1 # 统计所有顶点入度 |
| 211 | + |
| 212 | + # 将入度为 0 的顶点存入集合 S 中 |
| 213 | + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) |
| 214 | + order = [] # order 用于存储拓扑序列 |
| 215 | + |
| 216 | + while S: |
| 217 | + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 |
| 218 | + order.append(u) # 将其输出到拓扑序列 order 中 |
| 219 | + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v |
| 220 | + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 |
| 221 | + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 |
| 222 | + S.append(v) # 将其放入集合 S 中 |
| 223 | + |
| 224 | + if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 |
| 225 | + return [] |
| 226 | + return order # 返回拓扑序列 |
| 227 | + |
| 228 | + |
| 229 | + def findOrder(self, numCourses: int, prerequisites): |
| 230 | + graph = dict() |
| 231 | + for i in range(numCourses): |
| 232 | + graph[i] = [] |
| 233 | + |
| 234 | + for v, u in prerequisites: |
| 235 | + graph[u].append(v) |
| 236 | + |
| 237 | + return self.topologicalSortingKahn(graph) |
| 238 | +``` |
| 239 | + |
| 240 | +##### 思路 1:复杂度分析 |
| 241 | + |
| 242 | +- **时间复杂度**:$O(n + m)$,其中 $n$ 为课程数,$m$ 为先修课程的要求数。 |
| 243 | +- **空间复杂度**:$O(n + m)$。 |
| 244 | + |
| 245 | +### 3.2 找到最终的安全状态 |
| 246 | + |
| 247 | +#### 3.2.1 题目链接 |
| 248 | + |
| 249 | +- [802. 找到最终的安全状态 - 力扣](https://leetcode.cn/problems/find-eventual-safe-states/) |
| 250 | + |
| 251 | +#### 3.2.2 题目大意 |
| 252 | + |
| 253 | +**描述**:给定一个有向图 $graph$,其中 $graph[i]$ 是与节点 $i$ 相邻的节点列表,意味着从节点 $i$ 到节点 $graph[i]$ 中的每个节点都有一条有向边。 |
| 254 | + |
| 255 | +**要求**:找出图中所有的安全节点,将其存入数组作为答案返回,答案数组中的元素应当按升序排列。 |
| 256 | + |
| 257 | +**说明**: |
| 258 | + |
| 259 | +- **终端节点**:如果一个节点没有连出的有向边,则它是终端节点。或者说,如果没有出边,则节点为终端节点。 |
| 260 | +- **安全节点**:如果从该节点开始的所有可能路径都通向终端节点,则该节点为安全节点。 |
| 261 | +- $n == graph.length$。 |
| 262 | +- $1 \le n \le 10^4$。 |
| 263 | +- $0 \le graph[i].length \le n$。 |
| 264 | +- $0 \le graph[i][j] \le n - 1$。 |
| 265 | +- $graph[i]$ 按严格递增顺序排列。 |
| 266 | +- 图中可能包含自环。 |
| 267 | +- 图中边的数目在范围 $[1, 4 \times 10^4]$ 内。 |
| 268 | + |
| 269 | +**示例**: |
| 270 | + |
| 271 | +- 示例 1: |
| 272 | + |
| 273 | + |
| 274 | + |
| 275 | +```Python |
| 276 | +输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]] |
| 277 | +输出:[2,4,5,6] |
| 278 | +解释:示意图如上。 |
| 279 | +节点 5 和节点 6 是终端节点,因为它们都没有出边。 |
| 280 | +从节点 2、4、5 和 6 开始的所有路径都指向节点 5 或 6。 |
| 281 | +``` |
| 282 | + |
| 283 | +- 示例 2: |
| 284 | + |
| 285 | +```Python |
| 286 | +输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]] |
| 287 | +输出:[4] |
| 288 | +解释: |
| 289 | +只有节点 4 是终端节点,从节点 4 开始的所有路径都通向节点 4。 |
| 290 | +``` |
| 291 | + |
| 292 | +#### 3.2.3 解题思路 |
| 293 | + |
| 294 | +##### 思路 1:拓扑排序 |
| 295 | + |
| 296 | +1. 根据题意可知,安全节点所对应的终点,一定是出度为 $0$ 的节点。而安全节点一定能在有限步内到达终点,则说明安全节点一定不在「环」内。 |
| 297 | +2. 我们可以利用拓扑排序来判断顶点是否在环中。 |
| 298 | +3. 为了找出安全节点,可以采取逆序建图的方式,将所有边进行反向。这样出度为 $0$ 的终点就变为了入度为 $0$ 的点。 |
| 299 | +4. 然后通过拓扑排序不断移除入度为 $0$ 的点之后,如果不在「环」中的点,最后入度一定为 $0$,这些点也就是安全节点。而在「环」中的点,最后入度一定不为 $0$。 |
| 300 | +5. 最后将所有安全的起始节点存入数组作为答案返回。 |
| 301 | + |
| 302 | +##### 思路 1:代码 |
| 303 | + |
| 304 | +```Python |
| 305 | +class Solution: |
| 306 | + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) |
| 307 | + def topologicalSortingKahn(self, graph: dict): |
| 308 | + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有节点入度 |
| 309 | + for u in graph: |
| 310 | + for v in graph[u]: |
| 311 | + indegrees[v] += 1 # 统计所有节点入度 |
| 312 | + |
| 313 | + # 将入度为 0 的顶点存入集合 S 中 |
| 314 | + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) |
| 315 | + |
| 316 | + while S: |
| 317 | + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 |
| 318 | + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v |
| 319 | + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 |
| 320 | + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 |
| 321 | + S.append(v) # 将其放入集合 S 中 |
| 322 | + |
| 323 | + res = [] |
| 324 | + for u in indegrees: |
| 325 | + if indegrees[u] == 0: |
| 326 | + res.append(u) |
| 327 | + |
| 328 | + return res |
| 329 | + |
| 330 | + def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]: |
| 331 | + graph_dict = {u: [] for u in range(len(graph))} |
| 332 | + |
| 333 | + for u in range(len(graph)): |
| 334 | + for v in graph[u]: |
| 335 | + graph_dict[v].append(u) # 逆序建图 |
| 336 | + |
| 337 | + return self.topologicalSortingKahn(graph_dict) |
| 338 | +``` |
49 | 339 |
|
| 340 | +##### 思路 1:复杂度分析 |
50 | 341 |
|
| 342 | +- **时间复杂度**:$O(n + m)$,其中 $n$ 是图中节点数目,$m$ 是图中边数目。 |
| 343 | +- **空间复杂度**:$O(n + m)$。 |
0 commit comments