Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Assets/Origins/Categories-List.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions Contents/00.Introduction/05.Categories-List.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | 深度优先搜索、图、拓扑排序、数组 | 中等 |
Expand Down
310 changes: 303 additions & 7 deletions Contents/08.Graph/02.Graph-Traversal/05.Graph-Topological-Sorting.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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) | 深度优先搜索、图、拓扑排序、数组 | 中等 |
Expand Down
109 changes: 76 additions & 33 deletions Solutions/0210. 课程表 II.md
Original file line number Diff line number Diff line change
Expand Up @@ -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$,并且删除从该顶点出发的有向边 $<u, v>$(也就是把该顶点可达的顶点入度都减 $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)$。

111 changes: 77 additions & 34 deletions Solutions/0802. 找到最终的安全状态.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)$。

2 changes: 1 addition & 1 deletion Solutions/2246. 相邻字符不同的最长路径.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ class Solution:
### 思路 1:复杂度分析

- **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数目。
- **空间复杂度**:$O(n)$
- **空间复杂度**:$O(n)$
48 changes: 48 additions & 0 deletions Templates/08.Graph/Graph-Topological-Sorting-DFS.py
Original file line number Diff line number Diff line change
@@ -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, []))
45 changes: 41 additions & 4 deletions Templates/08.Graph/Graph-Topological-Sorting-Kahn.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,41 @@
class solution:
def topologicalSorting(graph):
indegrees = []
for
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)

print(Solution().findOrder(2, [[1,0]]))
print(Solution().findOrder(4, [[1,0],[2,0],[3,1],[3,2]]))
print(Solution().findOrder(1, []))