From 069aee5e051df1a224adc24a529acd58450ee2dc Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 09:10:21 +0800 Subject: [PATCH 1/7] =?UTF-8?q?Update=202246.=20=E7=9B=B8=E9=82=BB?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=8D=E5=90=8C=E7=9A=84=E6=9C=80=E9=95=BF?= =?UTF-8?q?=E8=B7=AF=E5=BE=84.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...232円204円346円234円200円351円225円277円350円267円257円345円276円204円.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Solutions/2246. 347円233円270円351円202円273円345円255円227円347円254円246円344円270円215円345円220円214円347円232円204円346円234円200円351円225円277円350円267円257円345円276円204円.md" "b/Solutions/2246. 347円233円270円351円202円273円345円255円227円347円254円246円344円270円215円345円220円214円347円232円204円346円234円200円351円225円277円350円267円257円345円276円204円.md" index 5e555a73..297983e2 100644 --- "a/Solutions/2246. 347円233円270円351円202円273円345円255円227円347円254円246円344円270円215円345円220円214円347円232円204円346円234円200円351円225円277円350円267円257円345円276円204円.md" +++ "b/Solutions/2246. 347円233円270円351円202円273円345円255円227円347円254円246円344円270円215円345円220円214円347円232円204円346円234円200円351円225円277円350円267円257円345円276円204円.md" @@ -98,4 +98,4 @@ class Solution: ### 思路 1:复杂度分析 - **时间复杂度**:$O(n),ドル其中 $n$ 是树的节点数目。 -- **空间复杂度**:$O(n)$ +- **空间复杂度**:$O(n)$。 From 3b50afc603f77a825dbc3de0f3967d82b7a6e39b Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 09:10:29 +0800 Subject: [PATCH 2/7] Update Graph-Topological-Sorting-Kahn.py --- .../Graph-Topological-Sorting-Kahn.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py b/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py index a0de3d80..65cd52c9 100644 --- a/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py +++ b/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py @@ -1,4 +1,25 @@ +import collections + class solution: def topologicalSorting(graph): - indegrees = [] - for \ No newline at end of file + indegrees = {u: 0 for u in graph} + for u in graph: + for v in graph[u]: + indegrees[v] += 1 + + + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) + order = [] + + while S: + u = S.pop() + order.append(u) + for v in graph[u]: + indegrees[v] -= 1 + if indegrees[v] == 0: + S.append(v) + + size = len(indegrees) + if size == len(S): + return order + return None \ No newline at end of file From f221d5386de43fa9e2506519eae8852995ed6f9d Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 16:53:32 +0800 Subject: [PATCH 3/7] =?UTF-8?q?Update=200210.=20=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E8=A1=A8=20II.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...350257円276円347円250円213円350円241円250円 II.md" | 109 ++++++++++++------ 1 file changed, 76 insertions(+), 33 deletions(-) diff --git "a/Solutions/0210. 350円257円276円347円250円213円350円241円250円 II.md" "b/Solutions/0210. 350円257円276円347円250円213円350円241円250円 II.md" index c0b751e5..8bd427f4 100644 --- "a/Solutions/0210. 350円257円276円347円250円213円350円241円250円 II.md" +++ "b/Solutions/0210. 350円257円276円347円250円213円350円241円250円 II.md" @@ -5,51 +5,94 @@ ## 题目大意 -给定一个整数 `numCourses`,代表这学期必须选修的课程数量,课程编号为 `0` 到 `numCourses - 1`。再给定一个数组 `prerequisites` 表示先修课程关系,其中 `prerequisites[i] = [ai, bi]` 表示如果要学习课程 `ai` 则必须要学习课程 `bi`。 +**描述**:给定一个整数 $numCourses,ドル代表这学期必须选修的课程数量,课程编号为 0ドル \sim numCourses - 1$。再给定一个数组 $prerequisites$ 表示先修课程关系,其中 $prerequisites[i] = [ai, bi]$ 表示如果要学习课程 $ai$ 则必须要学习课程 $bi$。 -要求:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 +**要求**:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 -## 解题思路 +**说明**: + +- 1ドル \le numCourses \le 2000$。 +- 0ドル \le prerequisites.length \le numCourses \times (numCourses - 1)$。 +- $prerequisites[i].length == 2$。 +- 0ドル \le ai, bi < numCourses$。 +- $ai \ne bi$。 +- 所有$[ai, bi]$ 互不相同。 + +**示例**: + +- 示例 1: + +```Python +输入:numCourses = 2, prerequisites = [[1,0]] +输出:[0,1] +解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1]。 +``` + +- 示例 2: -拓扑排序。这道题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的升级版,只需要在上一题的基础上增加一个答案数组即可。 +```Python +输入: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]。 +``` + +## 解题思路 -1. 使用列表 `edges` 存放课程关系图,并统计每门课程节点的入度,存入入度列表 `indegrees`。 +### 思路 1:拓扑排序 -2. 借助队列 `queue`,将所有入度为 `0` 的节点入队。 +这道题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的升级版,只需要在上一题的基础上增加一个答案数组 $order$ 即可。 -3. 从队列中选择一个节点,并将其加入到答案数组 `res` 中,再让课程数 -1。 -4. 将该顶点以及该顶点为出发点的所有边的另一个节点入度 -1。如果入度 -1 后的节点入度不为 0,则将其加入队列 `queue`。 -5. 重复 3~4 的步骤,直到队列中没有节点。 -6. 最后判断剩余课程数是否为 0,如果为 0,则返回答案数组 `res`,否则,返回空数组。 +1. 使用哈希表 $graph$ 存放课程关系图,并统计每门课程节点的入度,存入入度列表 $indegrees$。 +2. 借助队列 $S,ドル将所有入度为 0ドル$ 的节点入队。 +3. 从队列中选择一个节点 $u,ドル并将其加入到答案数组 $order$ 中。 +4. 从图中删除该顶点 $u,ドル并且删除从该顶点出发的有向边 $$(也就是把该顶点可达的顶点入度都减 1ドル$)。如果删除该边后顶点 $v$ 的入度变为 0ドル,ドル则将其加入队列 $S$ 中。 +5. 重复上述步骤 3ドル \sim 4,ドル直到队列中没有节点。 +6. 最后判断总的顶点数和拓扑序列中的顶点数是否相等,如果相等,则返回答案数组 $order,ドル否则,返回空数组。 -## 代码 +### 思路 1:代码 ```Python import collections class Solution: - def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]: - indegrees = [0 for _ in range(numCourses)] - edges = collections.defaultdict(list) - res = [] - for x, y in prerequisites: - edges[y].append(x) - indegrees[x] += 1 - queue = collections.deque([]) - for i in range(numCourses): - if not indegrees[i]: - queue.append(i) - while queue: - y = queue.popleft() - res.append(y) - numCourses -= 1 - for x in edges[y]: - indegrees[x] -= 1 - if not indegrees[x]: - queue.append(x) - if not numCourses: - return res - else: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingKahn(self, graph: dict): + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 + for u in graph: + for v in graph[u]: + indegrees[v] += 1 # 统计所有顶点入度 + + # 将入度为 0 的顶点存入集合 S 中 + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) + order = [] # order 用于存储拓扑序列 + + while S: + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 + order.append(u) # 将其输出到拓扑序列 order 中 + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 + S.append(v) # 将其放入集合 S 中 + + if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 return [] + return order # 返回拓扑序列 + + + def findOrder(self, numCourses: int, prerequisites): + graph = dict() + for i in range(numCourses): + graph[i] = [] + + for v, u in prerequisites: + graph[u].append(v) + + return self.topologicalSortingKahn(graph) ``` +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n + m),ドル其中 $n$ 为课程数,$m$ 为先修课程的要求数。 +- **空间复杂度**:$O(n + m)$。 + From 2f43b6dd6ce2beddbb528440994aa8af7604b9e1 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 16:53:33 +0800 Subject: [PATCH 4/7] =?UTF-8?q?Update=200802.=20=E6=89=BE=E5=88=B0?= =?UTF-8?q?=E6=9C=80=E7=BB=88=E7=9A=84=E5=AE=89=E5=85=A8=E7=8A=B6=E6=80=81?= =?UTF-8?q?.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...11345円205円250円347円212円266円346円200円201円.md" | 111 ++++++++++++------ 1 file changed, 77 insertions(+), 34 deletions(-) diff --git "a/Solutions/0802. 346円211円276円345円210円260円346円234円200円347円273円210円347円232円204円345円256円211円345円205円250円347円212円266円346円200円201円.md" "b/Solutions/0802. 346円211円276円345円210円260円346円234円200円347円273円210円347円232円204円345円256円211円345円205円250円347円212円266円346円200円201円.md" index ed0f2b57..da8c2314 100644 --- "a/Solutions/0802. 346円211円276円345円210円260円346円234円200円347円273円210円347円232円204円345円256円211円345円205円250円347円212円266円346円200円201円.md" +++ "b/Solutions/0802. 346円211円276円345円210円260円346円234円200円347円273円210円347円232円204円345円256円211円345円205円250円347円212円266円346円200円201円.md" @@ -5,52 +5,95 @@ ## 题目大意 -给定一个有向图 `graph`,其中 `graph[i]` 是编号 `j` 节点的一个列表,满足 `(i, j)` 是图的一条有向边。 +**描述**:给定一个有向图 $graph,ドル其中 $graph[i]$ 是与节点 $i$ 相邻的节点列表,意味着从节点 $i$ 到节点 $graph[i]$ 中的每个节点都有一条有向边。 -要求:找出图中所有安全的起始节点,将其存入数组作为答案返回。 +**要求**:找出图中所有的安全节点,将其存入数组作为答案返回,答案数组中的元素应当按升序排列。 -- 安全的起始节点:从该节点出发,无论每一步选择沿哪条有向边行走,最后必然再有限步内到达终点,则该起始节点称为安全的起始节点。 +**说明**: -## 解题思路 +- **终端节点**:如果一个节点没有连出的有向边,则它是终端节点。或者说,如果没有出边,则节点为终端节点。 +- **安全节点**:如果从该节点开始的所有可能路径都通向终端节点,则该节点为安全节点。 +- $n == graph.length$。 +- 1ドル \le n \le 10^4$。 +- 0ドル \le graph[i].length \le n$。 +- 0ドル \le graph[i][j] \le n - 1$。 +- $graph[i]$ 按严格递增顺序排列。 +- 图中可能包含自环。 +- 图中边的数目在范围 $[1, 4 \times 10^4]$ 内。 -根据题意可知,安全的起始节点所对应的终点,一定是出度为 0 的节点。而安全节点一定能在有限步内到达终点,则说明安全节点一定不在「环」内。 +**示例**: -我们可以利用拓扑排序来判断顶点是否在环中。为了找出起始节点,可以采取逆序建图的方式,将所有边进行反向。这样出度为 0 的终点就变为了入度为 0 的点。在通过拓扑排序不断移除入度为 0 的点之后,如果不在「环」中的点,最后入度一定为 0,这些点也就是安全的起始节点。而在「环」中的点,最后入度一定不为 0。 +- 示例 1: -然后将所有安全的起始节点存入数组作为答案返回。 +![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/03/17/picture1.png) -## 代码 +```Python +输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]] +输出:[2,4,5,6] +解释:示意图如上。 +节点 5 和节点 6 是终端节点,因为它们都没有出边。 +从节点 2、4、5 和 6 开始的所有路径都指向节点 5 或 6。 +``` + +- 示例 2: ```Python -import collections +输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]] +输出:[4] +解释: +只有节点 4 是终端节点,从节点 4 开始的所有路径都通向节点 4。 +``` -class Solution: - def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]: - size = len(graph) - indegrees = [0 for _ in range(size)] - edges = collections.defaultdict(list) - - for i in range(len(graph)): - for node in graph[i]: - x, y = i, node - edges[y].append(x) - indegrees[x] += 1 - queue = collections.deque([]) - for i in range(size): - if not indegrees[i]: - queue.append(i) - - while queue: - y = queue.popleft() - for x in edges[y]: - indegrees[x] -= 1 - if not indegrees[x]: - queue.append(x) +## 解题思路 + +### 思路 1:拓扑排序 +1. 根据题意可知,安全节点所对应的终点,一定是出度为 0ドル$ 的节点。而安全节点一定能在有限步内到达终点,则说明安全节点一定不在「环」内。 +2. 我们可以利用拓扑排序来判断顶点是否在环中。 +3. 为了找出安全节点,可以采取逆序建图的方式,将所有边进行反向。这样出度为 0ドル$ 的终点就变为了入度为 0ドル$ 的点。 +4. 然后通过拓扑排序不断移除入度为 0ドル$ 的点之后,如果不在「环」中的点,最后入度一定为 0ドル,ドル这些点也就是安全节点。而在「环」中的点,最后入度一定不为 0ドル$。 +5. 最后将所有安全的起始节点存入数组作为答案返回。 + +### 思路 1:代码 + +```Python +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingKahn(self, graph: dict): + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有节点入度 + for u in graph: + for v in graph[u]: + indegrees[v] += 1 # 统计所有节点入度 + + # 将入度为 0 的顶点存入集合 S 中 + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) + + while S: + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 + S.append(v) # 将其放入集合 S 中 + res = [] - for i in range(size): - if not indegrees[i]: - res.append(i) + for u in indegrees: + if indegrees[u] == 0: + res.append(u) + return res + + def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]: + graph_dict = {u: [] for u in range(len(graph))} + + for u in range(len(graph)): + for v in graph[u]: + graph_dict[v].append(u) # 逆序建图 + + return self.topologicalSortingKahn(graph_dict) ``` +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n + m),ドル其中 $n$ 是图中节点数目,$m$ 是图中边数目。 +- **空间复杂度**:$O(n + m)$。 + From 4688815a5f36ef63d50df78174e2277161ef0297 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 16:53:44 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8B=93=E6=89=91?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E4=BB=A3=E7=A0=81=E6=A8=A1=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../08.Graph/Graph-Topological-Sorting-DFS.py | 48 +++++++++++++++++++ .../Graph-Topological-Sorting-Kahn.py | 48 ++++++++++++------- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/Templates/08.Graph/Graph-Topological-Sorting-DFS.py b/Templates/08.Graph/Graph-Topological-Sorting-DFS.py index e69de29b..b4b742e0 100644 --- a/Templates/08.Graph/Graph-Topological-Sorting-DFS.py +++ b/Templates/08.Graph/Graph-Topological-Sorting-DFS.py @@ -0,0 +1,48 @@ +import collections + +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingDFS(self, graph: dict): + visited = set() # 记录当前顶点是否被访问过 + onStack = set() # 记录同一次深搜时,当前顶点是否被访问过 + order = [] # 用于存储拓扑序列 + hasCycle = False # 用于判断是否存在环 + + def dfs(u): + nonlocal hasCycle + if u in onStack: # 同一次深度优先搜索时,当前顶点被访问过,说明存在环 + hasCycle = True + if u in visited or hasCycle: # 当前节点被访问或者有环时直接返回 + return + + visited.add(u) # 标记节点被访问 + onStack.add(u) # 标记本次深搜时,当前顶点被访问 + + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + dfs(v) # 递归访问节点 v + + order.append(u) # 后序遍历顺序访问节点 u + onStack.remove(u) # 取消本次深搜时的 顶点访问标记 + + for u in graph: + if u not in visited: + dfs(u) # 递归遍历未访问节点 u + + if hasCycle: # 判断是否存在环 + return [] # 存在环,无法构成拓扑序列 + order.reverse() # 将后序遍历转为拓扑排序顺序 + return order # 返回拓扑序列 + + def findOrder(self, n: int, edges): + # 构建图 + graph = dict() + for i in range(n): + graph[i] = [] + for v, u in edges: + graph[u].append(v) + + return self.topologicalSortingDFS(graph) + +print(Solution().findOrder(2, [[1,0]])) +print(Solution().findOrder(4, [[1,0],[2,0],[3,1],[3,2]])) +print(Solution().findOrder(1, [])) \ No newline at end of file diff --git a/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py b/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py index 65cd52c9..93b3dc3e 100644 --- a/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py +++ b/Templates/08.Graph/Graph-Topological-Sorting-Kahn.py @@ -1,25 +1,41 @@ import collections -class solution: - def topologicalSorting(graph): - indegrees = {u: 0 for u in graph} +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingKahn(self, graph: dict): + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有节点入度 for u in graph: for v in graph[u]: - indegrees[v] += 1 - + indegrees[v] += 1 # 统计所有节点入度 + # 将入度为 0 的顶点存入集合 S 中 S = collections.deque([u for u in indegrees if indegrees[u] == 0]) - order = [] + order = [] # order 用于存储拓扑序列 while S: - u = S.pop() - order.append(u) - for v in graph[u]: - indegrees[v] -= 1 - if indegrees[v] == 0: - S.append(v) + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 + order.append(u) # 将其输出到拓扑序列 order 中 + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 + S.append(v) # 将其放入集合 S 中 - size = len(indegrees) - if size == len(S): - return order - return None \ No newline at end of file + if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 + return [] + return order # 返回拓扑序列 + + + def findOrder(self, n: int, edges): + # 构建图 + graph = dict() + for i in range(n): + graph[i] = [] + + for u, v in edges: + graph[u].append(v) + + return self.topologicalSortingKahn(graph) + +print(Solution().findOrder(2, [[1,0]])) +print(Solution().findOrder(4, [[1,0],[2,0],[3,1],[3,2]])) +print(Solution().findOrder(1, [])) \ No newline at end of file From 46c2a10d22216461492c49a65b5c406106d24656 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 16:53:53 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E9=A2=98=E8=A7=A3=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Origins/Categories-List.md | 2 +- .../00.Introduction/05.Categories-List.md | 1 + .../05.Graph-Topological-Sorting.md | 307 +++++++++++++++++- .../06.Graph-Topological-Sorting-List.md | 1 + 4 files changed, 303 insertions(+), 8 deletions(-) diff --git a/Assets/Origins/Categories-List.md b/Assets/Origins/Categories-List.md index 100c04e8..a3fdfa9d 100644 --- a/Assets/Origins/Categories-List.md +++ b/Assets/Origins/Categories-List.md @@ -201,7 +201,7 @@ ### [图的拓扑排序题目](../../Contents/08.Graph/02.Graph-Traversal/06.Graph-Topological-Sorting-List.md) -###### 0210. 课程表 II、0802. 找到最终的安全状态、0851. 喧闹和富有 +###### 0207. 课程表、0210. 课程表 II、0802. 找到最终的安全状态、0851. 喧闹和富有 ### [图的生成树题目](../../Contents/08.Graph/03.Gaph-Spanning-Tree/04.Gaph-Spanning-Tree-List.md) diff --git a/Contents/00.Introduction/05.Categories-List.md b/Contents/00.Introduction/05.Categories-List.md index 57adb80f..90b7a169 100644 --- a/Contents/00.Introduction/05.Categories-List.md +++ b/Contents/00.Introduction/05.Categories-List.md @@ -642,6 +642,7 @@ | 题号 | 标题 | 题解 | 标签 | 难度 | | :------ | :------ | :------ | :------ | :------ | +| 0207 | [课程表](https://leetcode.cn/problems/course-schedule/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0207.%20%E8%AF%BE%E7%A8%8B%E8%A1%A8.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | 0210 | [课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0210.%20%E8%AF%BE%E7%A8%8B%E8%A1%A8%20II.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | 0802 | [找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0802.%20%E6%89%BE%E5%88%B0%E6%9C%80%E7%BB%88%E7%9A%84%E5%AE%89%E5%85%A8%E7%8A%B6%E6%80%81.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | 0851 | [喧闹和富有](https://leetcode.cn/problems/loud-and-rich/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0851.%20%E5%96%A7%E9%97%B9%E5%92%8C%E5%AF%8C%E6%9C%89.md) | 深度优先搜索、图、拓扑排序、数组 | 中等 | diff --git a/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md b/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md index 57a62c0e..3ef7a3c2 100644 --- a/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md +++ b/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md @@ -10,41 +10,334 @@ ## 2. 拓扑排序的实现方法 -拓扑排序有两种实现方法,分别是 Kahn 算法和 DFS 深度优先搜索算法。接下来我们依次来看下它们是如何实现的。 +拓扑排序有两种实现方法,分别是「Kahn 算法」和「DFS 深度优先搜索算法」。接下来我们依次来看下它们是如何实现的。 ### 2.1 Kahn 算法 +> **Kahn 算法的基本思想**: + #### 2.1.1 Kahn 算法的实现步骤 1. 使用数组 $indegrees$ 用于记录图中各个顶点的入度。 2. 维护一个入度为 0ドル$ 的顶点集合 $S$(可使用栈、队列、优先队列)。 3. 每次从集合中选择任何一个没有前驱(即入度为 0ドル$)的顶点 $u,ドル将其输出到拓扑序列 $order$ 中。 4. 从图中删除该顶点 $u,ドル并且删除从该顶点出发的有向边 $$(也就是把该顶点可达的顶点入度都减 1ドル$)。如果删除该边后顶点 $v$ 的入度变为 0ドル,ドル则将顶点 $v$ 放入集合 $S$ 中。 -5. 重复上述过程,直到集合 $S$ 为空。 -6. 检测图中是否存在任何边,如果有,则该图一定存在环路。否则 $order$ 中顶点的顺序就是拓扑排序的结果。 +5. 重复上述过程,直到集合 $S$ 为空,或者图中还有顶点未被访问(说明一定存在环路,无法形成拓扑序列)。 +6. 如果不存在环路,则 $order$ 中顶点的顺序就是拓扑排序的结果。 #### 2.1.2 Kahn 算法的实现代码 ```Python +import collections +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingKahn(self, graph: dict): + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 + for u in graph: + for v in graph[u]: + indegrees[v] += 1 # 统计所有顶点入度 + + # 将入度为 0 的顶点存入集合 S 中 + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) + order = [] # order 用于存储拓扑序列 + + while S: + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 + order.append(u) # 将其输出到拓扑序列 order 中 + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 + S.append(v) # 将其放入集合 S 中 + + if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 + return [] + return order # 返回拓扑序列 + + + def findOrder(self, n: int, edges): + # 构建图 + graph = dict() + for i in range(n): + graph[i] = [] + + for u, v in edges: + graph[u].append(v) + + return self.topologicalSortingKahn(graph) ``` -### 2.2 DFS 深度优先搜索算法 +### 2.2 基于 DFS 实现拓扑排序算法 + +> **基于 DFS 实现拓扑排序算法的基本思想**: +> +> 1. 对于一个顶点 $u,ドル深度游先生遍历从该顶点出发的有向边 $$。如果从该顶点 $u$ 出发的所有相邻顶点 $v$ 都已经搜索完毕,则在搜索回溯到顶点 $u$ 时,$u$ 本身也会编程一个已经搜索完的顶点。 +> 2. 在拓扑排序的序列中,该顶点 $u$ 位于其所有相邻顶点 $v$ 的前面。 +> 3. 这样一来,我们对每个顶点进行回溯时,将其放入栈中,则最终从栈顶到栈底的序列就是一种拓扑排序。 -#### 2.2.1 DFS 深度优先搜索算法实现步骤 +#### 2.2.1 基于 DFS 实现拓扑排序算法实现步骤 -1. 以任意顺序循环遍历图中的每个顶点,将其输出到拓扑序列中。 -2. 如果搜索时遇到之前已经遇到的顶点,或者碰到叶节点,则中止算法。 +1. 使用集合 $visited$ 用于记录当前顶点是否被访问过,避免重复访问。 +2. 使用集合 $onStack$ 用于记录同一次深度优先搜索时,当前顶点是否被访问过。如果当前顶点被访问过,则说明图中存在环路,无法构成拓扑序列。 +3. 使用布尔变量 $hasCycle$ 用于判断图中是否存在环。 +4. 从任意一个未被访问的顶点 $u$ 出发。 + 1. 如果顶点 $u$ 在同一次深度优先搜索时被访问过,则说明存在环。 + 2. 如果当前顶点被访问或者有环时,则无需再继续遍历,直接返回。 + +5. 将顶点 $u$ 标记为被访问过,并在本次深度优先搜索中标记为访问过。然后深度游先生遍历从顶点 $u$ 出发的有向边 $$。 +6. 当顶点 $u$ 的所有相邻顶点 $v$ 都被访问后,回溯前记录当前节点 $u$(将当前节点 $u$ 输出到拓扑序列 $order$ 中)。 +7. 取消本次深度优先搜索时,顶点 $u$ 的访问标记。 +8. 对其他未被访问的顶点重复 4ドル \sim 7$ 步过程,直到所有节点都遍历完,或者出现环。 +9. 如果不存在环路,则将 $order$ 逆序排序后,顶点的顺序就是拓扑排序的结果。 #### 2.2.2 DFS 深度优先搜索算法实现代码 ```Python +import collections +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingDFS(self, graph: dict): + visited = set() # 记录当前顶点是否被访问过 + onStack = set() # 记录同一次深搜时,当前顶点是否被访问过 + order = [] # 用于存储拓扑序列 + hasCycle = False # 用于判断是否存在环 + + def dfs(u): + nonlocal hasCycle + if u in onStack: # 同一次深度优先搜索时,当前顶点被访问过,说明存在环 + hasCycle = True + if u in visited or hasCycle: # 当前节点被访问或者有环时直接返回 + return + + visited.add(u) # 标记节点被访问 + onStack.add(u) # 标记本次深搜时,当前顶点被访问 + + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + dfs(v) # 递归访问节点 v + + order.append(u) # 后序遍历顺序访问节点 u + onStack.remove(u) # 取消本次深搜时的 顶点访问标记 + + for u in graph: + if u not in visited: + dfs(u) # 递归遍历未访问节点 u + + if hasCycle: # 判断是否存在环 + return [] # 存在环,无法构成拓扑序列 + order.reverse() # 将后序遍历转为拓扑排序顺序 + return order # 返回拓扑序列 + + def findOrder(self, n: int, edges): + # 构建图 + graph = dict() + for i in range(n): + graph[i] = [] + for v, u in edges: + graph[u].append(v) + + return self.topologicalSortingDFS(graph) ``` ## 3. 拓扑排序的应用 拓扑排序可以用来解决一些依赖关系的问题,比如项目的执行顺序,课程的选修顺序等。 +### 3.1 课程表 II + +#### 3.1.1 题目链接 + +- [210. 课程表 II - 力扣](https://leetcode.cn/problems/course-schedule-ii/) + +#### 3.1.2 题目大意 + +**描述**:给定一个整数 $numCourses,ドル代表这学期必须选修的课程数量,课程编号为 0ドル \sim numCourses - 1$。再给定一个数组 $prerequisites$ 表示先修课程关系,其中 $prerequisites[i] = [ai, bi]$ 表示如果要学习课程 $ai$ 则必须要学习课程 $bi$。 + +**要求**:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 + +**说明**: + +- 1ドル \le numCourses \le 2000$。 +- 0ドル \le prerequisites.length \le numCourses \times (numCourses - 1)$。 +- $prerequisites[i].length == 2$。 +- 0ドル \le ai, bi < numCourses$。 +- $ai \ne bi$。 +- 所有$[ai, bi]$ 互不相同。 + +**示例**: + +- 示例 1: + +```Python +输入:numCourses = 2, prerequisites = [[1,0]] +输出:[0,1] +解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1]。 +``` + +- 示例 2: + +```Python +输入: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.1.3 解题思路 + +##### 思路 1:拓扑排序 + +这道题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的升级版,只需要在上一题的基础上增加一个答案数组 $order$ 即可。 + +1. 使用哈希表 $graph$ 存放课程关系图,并统计每门课程节点的入度,存入入度列表 $indegrees$。 +2. 借助队列 $S,ドル将所有入度为 0ドル$ 的节点入队。 +3. 从队列中选择一个节点 $u,ドル并将其加入到答案数组 $order$ 中。 +4. 从图中删除该顶点 $u,ドル并且删除从该顶点出发的有向边 $$(也就是把该顶点可达的顶点入度都减 1ドル$)。如果删除该边后顶点 $v$ 的入度变为 0ドル,ドル则将其加入队列 $S$ 中。 +5. 重复上述步骤 3ドル \sim 4,ドル直到队列中没有节点。 +6. 最后判断总的顶点数和拓扑序列中的顶点数是否相等,如果相等,则返回答案数组 $order,ドル否则,返回空数组。 + +##### 思路 1:代码 + +```Python +import collections + +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingKahn(self, graph: dict): + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 + for u in graph: + for v in graph[u]: + indegrees[v] += 1 # 统计所有顶点入度 + + # 将入度为 0 的顶点存入集合 S 中 + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) + order = [] # order 用于存储拓扑序列 + + while S: + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 + order.append(u) # 将其输出到拓扑序列 order 中 + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 + S.append(v) # 将其放入集合 S 中 + + if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 + return [] + return order # 返回拓扑序列 + + + def findOrder(self, numCourses: int, prerequisites): + graph = dict() + for i in range(numCourses): + graph[i] = [] + + for v, u in prerequisites: + graph[u].append(v) + + return self.topologicalSortingKahn(graph) +``` + +##### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n + m),ドル其中 $n$ 为课程数,$m$ 为先修课程的要求数。 +- **空间复杂度**:$O(n + m)$。 + +### 3.2 找到最终的安全状态 + +#### 3.2.1 题目链接 + +- [802. 找到最终的安全状态 - 力扣](https://leetcode.cn/problems/find-eventual-safe-states/) + +#### 3.2.2 题目大意 + +**描述**:给定一个有向图 $graph,ドル其中 $graph[i]$ 是与节点 $i$ 相邻的节点列表,意味着从节点 $i$ 到节点 $graph[i]$ 中的每个节点都有一条有向边。 + +**要求**:找出图中所有的安全节点,将其存入数组作为答案返回,答案数组中的元素应当按升序排列。 + +**说明**: + +- **终端节点**:如果一个节点没有连出的有向边,则它是终端节点。或者说,如果没有出边,则节点为终端节点。 +- **安全节点**:如果从该节点开始的所有可能路径都通向终端节点,则该节点为安全节点。 +- $n == graph.length$。 +- 1ドル \le n \le 10^4$。 +- 0ドル \le graph[i].length \le n$。 +- 0ドル \le graph[i][j] \le n - 1$。 +- $graph[i]$ 按严格递增顺序排列。 +- 图中可能包含自环。 +- 图中边的数目在范围 $[1, 4 \times 10^4]$ 内。 + +**示例**: + +- 示例 1: + +![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/03/17/picture1.png) + +```Python +输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]] +输出:[2,4,5,6] +解释:示意图如上。 +节点 5 和节点 6 是终端节点,因为它们都没有出边。 +从节点 2、4、5 和 6 开始的所有路径都指向节点 5 或 6。 +``` + +- 示例 2: + +```Python +输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]] +输出:[4] +解释: +只有节点 4 是终端节点,从节点 4 开始的所有路径都通向节点 4。 +``` + +#### 3.2.3 解题思路 + +##### 思路 1:拓扑排序 + +1. 根据题意可知,安全节点所对应的终点,一定是出度为 0ドル$ 的节点。而安全节点一定能在有限步内到达终点,则说明安全节点一定不在「环」内。 +2. 我们可以利用拓扑排序来判断顶点是否在环中。 +3. 为了找出安全节点,可以采取逆序建图的方式,将所有边进行反向。这样出度为 0ドル$ 的终点就变为了入度为 0ドル$ 的点。 +4. 然后通过拓扑排序不断移除入度为 0ドル$ 的点之后,如果不在「环」中的点,最后入度一定为 0ドル,ドル这些点也就是安全节点。而在「环」中的点,最后入度一定不为 0ドル$。 +5. 最后将所有安全的起始节点存入数组作为答案返回。 + +##### 思路 1:代码 + +```Python +class Solution: + # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) + def topologicalSortingKahn(self, graph: dict): + indegrees = {u: 0 for u in graph} # indegrees 用于记录所有节点入度 + for u in graph: + for v in graph[u]: + indegrees[v] += 1 # 统计所有节点入度 + + # 将入度为 0 的顶点存入集合 S 中 + S = collections.deque([u for u in indegrees if indegrees[u] == 0]) + + while S: + u = S.pop() # 从集合中选择一个没有前驱的顶点 0 + for v in graph[u]: # 遍历顶点 u 的邻接顶点 v + indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 + if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 + S.append(v) # 将其放入集合 S 中 + + res = [] + for u in indegrees: + if indegrees[u] == 0: + res.append(u) + + return res + + def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]: + graph_dict = {u: [] for u in range(len(graph))} + + for u in range(len(graph)): + for v in graph[u]: + graph_dict[v].append(u) # 逆序建图 + + return self.topologicalSortingKahn(graph_dict) +``` +##### 思路 1:复杂度分析 +- **时间复杂度**:$O(n + m),ドル其中 $n$ 是图中节点数目,$m$ 是图中边数目。 +- **空间复杂度**:$O(n + m)$。 diff --git a/Contents/08.Graph/02.Graph-Traversal/06.Graph-Topological-Sorting-List.md b/Contents/08.Graph/02.Graph-Traversal/06.Graph-Topological-Sorting-List.md index 76404d76..ba29086c 100644 --- a/Contents/08.Graph/02.Graph-Traversal/06.Graph-Topological-Sorting-List.md +++ b/Contents/08.Graph/02.Graph-Traversal/06.Graph-Topological-Sorting-List.md @@ -2,6 +2,7 @@ | 题号 | 标题 | 题解 | 标签 | 难度 | | :------ | :------ | :------ | :------ | :------ | +| 0207 | [课程表](https://leetcode.cn/problems/course-schedule/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0207.%20%E8%AF%BE%E7%A8%8B%E8%A1%A8.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | 0210 | [课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0210.%20%E8%AF%BE%E7%A8%8B%E8%A1%A8%20II.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | 0802 | [找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0802.%20%E6%89%BE%E5%88%B0%E6%9C%80%E7%BB%88%E7%9A%84%E5%AE%89%E5%85%A8%E7%8A%B6%E6%80%81.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | 0851 | [喧闹和富有](https://leetcode.cn/problems/loud-and-rich/) | [Python](https://github.com/itcharge/LeetCode-Py/blob/main/Solutions/0851.%20%E5%96%A7%E9%97%B9%E5%92%8C%E5%AF%8C%E6%9C%89.md) | 深度优先搜索、图、拓扑排序、数组 | 中等 | From 29f0aefa70c1ee88443e6088adda824c4f5c9648 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 5 May 2023 17:03:24 +0800 Subject: [PATCH 7/7] Update 05.Graph-Topological-Sorting.md --- .../02.Graph-Traversal/05.Graph-Topological-Sorting.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md b/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md index 3ef7a3c2..ddd5396f 100644 --- a/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md +++ b/Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md @@ -15,6 +15,10 @@ ### 2.1 Kahn 算法 > **Kahn 算法的基本思想**: +> +> 1. 不断找寻有向图中入度为 0ドル$ 的顶点,将其输出。 +> 2. 然后删除入度为 0ドル$ 的顶点和从该顶点出发的有向边。 +> 3. 重复上述操作直到图为空,或者找不到入度为 0ドル$ 的节点为止。 #### 2.1.1 Kahn 算法的实现步骤 @@ -71,9 +75,8 @@ class Solution: > **基于 DFS 实现拓扑排序算法的基本思想**: > -> 1. 对于一个顶点 $u,ドル深度游先生遍历从该顶点出发的有向边 $$。如果从该顶点 $u$ 出发的所有相邻顶点 $v$ 都已经搜索完毕,则在搜索回溯到顶点 $u$ 时,$u$ 本身也会编程一个已经搜索完的顶点。 -> 2. 在拓扑排序的序列中,该顶点 $u$ 位于其所有相邻顶点 $v$ 的前面。 -> 3. 这样一来,我们对每个顶点进行回溯时,将其放入栈中,则最终从栈顶到栈底的序列就是一种拓扑排序。 +> 1. 对于一个顶点 $u,ドル深度优先遍历从该顶点出发的有向边 $$。如果从该顶点 $u$ 出发的所有相邻顶点 $v$ 都已经搜索完毕,则回溯到顶点 $u$ 时,该顶点 $u$ 应该位于其所有相邻顶点 $v$ 的前面(拓扑序列中)。 +> 2. 这样一来,当我们对每个顶点进行深度优先搜索,在回溯到该顶点时将其放入栈中,则最终从栈顶到栈底的序列就是一种拓扑排序。 #### 2.2.1 基于 DFS 实现拓扑排序算法实现步骤

AltStyle によって変換されたページ (->オリジナル) /