feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。 第01章 向量 | 第02章 矩阵 | 第03章 微积分 第04章 统计学 | 第05章 概率论 | 第06章 机器学习 第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音 第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络 第13章 计算与操作系统 | 第14章 数据结构与算法 第15章 生产级软件工程 | 第16章 SIMD与GPU编程 第17章 AI推理 | 第18章 ML系统设计 第19章 应用人工智能 | 第20章 前沿人工智能 翻译说明: - 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留 - mkdocs.yml 配置中文导航 + language: zh - README.md 已翻译为中文(兼 docs/index.md) - docs/ 目录包含指向各章文件的 symlink - 约 29,000 行中文内容,排除 .cache/ 构建缓存
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# 图
|
||||
|
||||
*图建模了关系和连接——从社交网络到道路地图再到依赖链。本文件涵盖图的表示、BFS、DFS、最短路径、拓扑排序和连通分量,包括遍历和寻路模式,这些是图面试题中的核心。*
|
||||
|
||||
- 我们在第12章(邻接矩阵、拉普拉斯矩阵、谱性质)和第13章(树、平面性、着色)中已经介绍了图论。这里我们专注于**算法模式**:如何在代码中遍历、搜索和优化图。
|
||||
|
||||
- 两种基本的图算法是 **BFS** 和 **DFS**。几乎所有图问题都可以归结为其中一种,可能带有修改。掌握这两种算法,你就能解决绝大多数图问题。
|
||||
|
||||
## 图的表示
|
||||
|
||||
- **邻接表**:对于每个节点,存储一个邻居列表。空间:$O(|V| + |E|)$。最适合稀疏图(大多数现实世界的图)。
|
||||
|
||||
```python
|
||||
# 无向图
|
||||
graph = {
|
||||
0: [1, 2],
|
||||
1: [0, 3],
|
||||
2: [0, 3],
|
||||
3: [1, 2]
|
||||
}
|
||||
|
||||
# 从边列表构建
|
||||
def build_graph(n, edges):
|
||||
graph = {i: [] for i in range(n)}
|
||||
for u, v in edges:
|
||||
graph[u].append(v)
|
||||
graph[v].append(u) # 有向图省略这一行
|
||||
return graph
|
||||
```
|
||||
|
||||
- **邻接矩阵**:$n \times n$ 矩阵,其中 $A[i][j] = 1$ 如果边 $(i, j)$ 存在。空间:$O(|V|^2)$。最适合稠密图或需要 $O(1)$ 边查找时。
|
||||
|
||||
- **何时使用哪种**:绝大多数情况使用邻接表。只有当图很稠密($|E| \approx |V|^2$)或需要常数时间边存在性检查时才使用矩阵。
|
||||
|
||||
## 模式:BFS(广度优先搜索)
|
||||
|
||||
- BFS 使用队列**逐层**探索节点。它是以下问题的首选算法:
|
||||
- **无权**图中的最短路径
|
||||
- 层序遍历
|
||||
- 寻找连通分量
|
||||
- 任何询问"最小步数"的问题
|
||||
|
||||
```python
|
||||
from collections import deque
|
||||
|
||||
def bfs(graph, start):
|
||||
visited = {start}
|
||||
queue = deque([start])
|
||||
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
for neighbour in graph[node]:
|
||||
if neighbour not in visited:
|
||||
visited.add(neighbour)
|
||||
queue.append(neighbour)
|
||||
```
|
||||
|
||||
- **关键**:在**入队时**添加到 `visited`,而不是在出队时。如果你在出队时标记已访问,同一个节点可能被不同前驱多次入队,浪费时间并可能导致错误结果。
|
||||
|
||||
### 简单:岛屿数量
|
||||
|
||||
- **问题**:给定一个由 '1'(陆地)和 '0'(水)组成的 2D 网格,计算岛屿的数量。
|
||||
|
||||
- **模式**:遍历网格。当找到一个 '1' 时,使用 BFS/DFS 将所有连通的陆地单元格标记为已访问。每次开始 BFS 就是一个岛屿。
|
||||
|
||||
```python
|
||||
from collections import deque
|
||||
|
||||
def num_islands(grid):
|
||||
if not grid:
|
||||
return 0
|
||||
|
||||
rows, cols = len(grid), len(grid[0])
|
||||
count = 0
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if grid[r][c] == '1':
|
||||
count += 1
|
||||
# BFS 标记整个岛屿
|
||||
queue = deque([(r, c)])
|
||||
grid[r][c] = '0' # 标记已访问
|
||||
while queue:
|
||||
cr, cc = queue.popleft()
|
||||
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
|
||||
nr, nc = cr + dr, cc + dc
|
||||
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
|
||||
grid[nr][nc] = '0'
|
||||
queue.append((nr, nc))
|
||||
|
||||
return count
|
||||
```
|
||||
|
||||
- **陷阱**:`directions = [(0,1),(0,-1),(1,0),(-1,0)]` 模式用于四连通网格邻居,几乎每个网格问题都会用到。记住它。对于八连通,加上对角线。
|
||||
|
||||
- **陷阱**:修改输入网格(`grid[r][c] = '0'`)避免了需要单独的 `visited` 集合。在面试中这是可以接受的,但要明确说明权衡(改变了输入)。
|
||||
|
||||
### 中等:腐烂的橘子
|
||||
|
||||
- **问题**:新鲜橘子如果与腐烂橘子相邻则会腐烂。返回所有橘子都腐烂的最短时间(如果不可能则返回 -1)。
|
||||
|
||||
- **模式**:多源 BFS。将所有初始腐烂的橘子同时放入队列。每层 BFS 就是一个时间步。
|
||||
|
||||
```python
|
||||
from collections import deque
|
||||
|
||||
def oranges_rotting(grid):
|
||||
rows, cols = len(grid), len(grid[0])
|
||||
queue = deque()
|
||||
fresh = 0
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if grid[r][c] == 2:
|
||||
queue.append((r, c))
|
||||
elif grid[r][c] == 1:
|
||||
fresh += 1
|
||||
|
||||
if fresh == 0:
|
||||
return 0
|
||||
|
||||
time = 0
|
||||
while queue and fresh > 0:
|
||||
time += 1
|
||||
for _ in range(len(queue)):
|
||||
cr, cc = queue.popleft()
|
||||
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
|
||||
nr, nc = cr + dr, cc + dc
|
||||
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
|
||||
grid[nr][nc] = 2
|
||||
fresh -= 1
|
||||
queue.append((nr, nc))
|
||||
|
||||
return time if fresh == 0 else -1
|
||||
```
|
||||
|
||||
- **关键洞察**:多源 BFS 同时处理所有源。这给出了从*任何*源的最短距离,这正是"最后一个新鲜橘子腐烂需要多长时间"。
|
||||
|
||||
## 模式:DFS(深度优先搜索)
|
||||
|
||||
- DFS 尽可能深地探索,然后回溯。它使用栈(显式栈或通过递归使用调用栈)。DFS 是以下问题的首选:
|
||||
- 环检测
|
||||
- 拓扑排序
|
||||
- 连通分量
|
||||
- 回溯 / 穷举搜索
|
||||
- 带约束的寻路
|
||||
|
||||
```python
|
||||
def dfs(graph, node, visited=None):
|
||||
if visited is None:
|
||||
visited = set()
|
||||
visited.add(node)
|
||||
for neighbour in graph[node]:
|
||||
if neighbour not in visited:
|
||||
dfs(graph, neighbour, visited)
|
||||
```
|
||||
|
||||
### 中等:课程表(环检测)
|
||||
|
||||
- **问题**:给定 $n$ 门课程和先修条件,判断是否能完成所有课程(即,没有循环依赖)。
|
||||
|
||||
- **模式**:在有向图中检测环。使用带有三种状态的 DFS:未访问、正在进行(在当前 DFS 路径上)、已完成。
|
||||
|
||||
```python
|
||||
def can_finish(num_courses, prerequisites):
|
||||
graph = {i: [] for i in range(num_courses)}
|
||||
for course, prereq in prerequisites:
|
||||
graph[course].append(prereq)
|
||||
|
||||
# 0 = 未访问, 1 = 进行中, 2 = 已完成
|
||||
state = [0] * num_courses
|
||||
|
||||
def has_cycle(node):
|
||||
if state[node] == 1:
|
||||
return True # 回边 → 环
|
||||
if state[node] == 2:
|
||||
return False # 已经完全探索过
|
||||
|
||||
state[node] = 1 # 标记为进行中
|
||||
for neighbour in graph[node]:
|
||||
if has_cycle(neighbour):
|
||||
return True
|
||||
state[node] = 2 # 标记为已完成
|
||||
return False
|
||||
|
||||
for course in range(num_courses):
|
||||
if has_cycle(course):
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
- **为什么需要三种状态**:两种状态(已访问/未访问)无法区分"我正在探索这个节点"和"我已完成对这个节点的探索"。找到一个当前正在被探索的节点(状态 = 1)意味着我们发现了环。找到一个已经完全探索的节点(状态 = 2)只是交叉边,不是环。
|
||||
|
||||
### 中等:课程表 II(拓扑排序)
|
||||
|
||||
- **问题**:返回一个有效的课程顺序(拓扑排序)。
|
||||
|
||||
- **模式(Kahn 算法——基于 BFS)**:从没有入边的节点(入度为 0)开始。处理它们,减少它们邻居的入度。重复。
|
||||
|
||||
```python
|
||||
from collections import deque
|
||||
|
||||
def find_order(num_courses, prerequisites):
|
||||
graph = {i: [] for i in range(num_courses)}
|
||||
indegree = [0] * num_courses
|
||||
|
||||
for course, prereq in prerequisites:
|
||||
graph[prereq].append(course)
|
||||
indegree[course] += 1
|
||||
|
||||
queue = deque([i for i in range(num_courses) if indegree[i] == 0])
|
||||
order = []
|
||||
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
order.append(node)
|
||||
for neighbour in graph[node]:
|
||||
indegree[neighbour] -= 1
|
||||
if indegree[neighbour] == 0:
|
||||
queue.append(neighbour)
|
||||
|
||||
return order if len(order) == num_courses else [] # 空 = 存在环
|
||||
```
|
||||
|
||||
- **陷阱**:如果结果中的节点数少于图中的节点数,则存在环(某些节点的入度从未降到 0)。
|
||||
|
||||
## 最短路径
|
||||
|
||||
### Dijkstra 算法
|
||||
|
||||
- 在**非负**加权图中从源点找到到所有其他节点的最短路径。使用优先队列(最小堆)。
|
||||
|
||||
```python
|
||||
import heapq
|
||||
|
||||
def dijkstra(graph, start):
|
||||
# graph: {node: [(neighbour, weight), ...]}
|
||||
dist = {node: float('inf') for node in graph}
|
||||
dist[start] = 0
|
||||
heap = [(0, start)]
|
||||
|
||||
while heap:
|
||||
d, node = heapq.heappop(heap)
|
||||
if d > dist[node]:
|
||||
continue # 过期条目
|
||||
|
||||
for neighbour, weight in graph[node]:
|
||||
new_dist = d + weight
|
||||
if new_dist < dist[neighbour]:
|
||||
dist[neighbour] = new_dist
|
||||
heapq.heappush(heap, (new_dist, neighbour))
|
||||
|
||||
return dist
|
||||
```
|
||||
|
||||
- 时间:使用二叉堆为 $O((|V| + |E|) \log |V|)$。
|
||||
|
||||
- **陷阱**:`if d > dist[node]: continue` 这行是必须的。没有它,你会处理过期的堆条目,可能退化到 $O(|V|^2)$。
|
||||
|
||||
- **陷阱**:Dijkstra 不适用于负权重。如果一条边有负权重,贪心假设(一旦节点被确定,其距离就是最优的)就不成立了。应改用 Bellman-Ford。
|
||||
|
||||
### 困难:网络延迟时间
|
||||
|
||||
- **问题**:给定 $n$ 个节点和加权有向边,找出信号从源点到达所有节点所需的时间。如果并非所有节点都可到达,返回 -1。
|
||||
|
||||
```python
|
||||
def network_delay(times, n, k):
|
||||
graph = {i: [] for i in range(1, n + 1)}
|
||||
for u, v, w in times:
|
||||
graph[u].append((v, w))
|
||||
|
||||
dist = dijkstra(graph, k)
|
||||
max_time = max(dist.values())
|
||||
return max_time if max_time < float('inf') else -1
|
||||
```
|
||||
|
||||
## 强连通分量
|
||||
|
||||
- 在有向图中,**强连通分量(SCC)**是一个最大节点集合,其中每个节点都能到达其他所有节点。
|
||||
|
||||
- **Kosaraju 算法**:(1) 在原始图上进行 DFS,记录完成顺序。(2) 转置图(反转所有边)。(3) 按完成顺序的逆序在转置图上进行 DFS。第3步中的每个 DFS 树就是一个 SCC。
|
||||
|
||||
- **何时使用**:寻找循环依赖、2-SAT、将有向图压缩为 SCC 的 DAG。
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱总结
|
||||
|
||||
| 陷阱 | 示例 | 修复 |
|
||||
|---------|---------|-----|
|
||||
| 在出队时标记已访问 | 同一节点被多次入队 | 在入队时标记已访问 |
|
||||
| 有向图中只有两种状态 | 无法区分回边和交叉边 | 使用三种状态:未访问/进行中/已完成 |
|
||||
| Dijkstra 用于负权重 | 错误的最短路径 | 使用 Bellman-Ford |
|
||||
| 忘记 `if d > dist[node]: continue` | 处理过期堆条目 | 总是跳过当前距离更差的情况 |
|
||||
| 网格边界检查 | 索引越界 | `0 <= nr < rows and 0 <= nc < cols` |
|
||||
| 没有考虑 time=0 的边界情况 | 腐烂橘子:没有新鲜橘子 | 在 BFS 之前检查 `fresh == 0` |
|
||||
| 将有向图构建为无向图 | 先修条件是单向的 | 只在一个方向添加边 |
|
||||
|
||||
---
|
||||
|
||||
## 课后练习题(NeetCode)
|
||||
|
||||
### BFS 模式
|
||||
- [岛屿数量](https://neetcode.io/problems/count-number-of-islands) — 网格 BFS/DFS
|
||||
- [腐烂的橘子](https://neetcode.io/problems/rotting-fruit) — 多源 BFS
|
||||
- [克隆图](https://neetcode.io/problems/clone-graph) — BFS + 哈希表克隆
|
||||
- [太平洋大西洋水流](https://neetcode.io/problems/pacific-atlantic-water-flow) — 从两个海洋开始的 BFS
|
||||
- [单词接龙](https://neetcode.io/problems/word-ladder) — 隐式图上的 BFS
|
||||
|
||||
### DFS 模式
|
||||
- [岛屿的最大面积](https://neetcode.io/problems/max-area-of-island) — 带面积计数的 DFS
|
||||
- [课程表](https://neetcode.io/problems/course-schedule) — 有向图中的环检测
|
||||
- [课程表 II](https://neetcode.io/problems/course-schedule-ii) — 拓扑排序
|
||||
- [连通分量数量](https://neetcode.io/problems/count-connected-components) — DFS 或并查集
|
||||
- [图是否是树](https://neetcode.io/problems/valid-tree) — 连通 + 无环
|
||||
|
||||
### 最短路径
|
||||
- [网络延迟时间](https://neetcode.io/problems/network-delay-time) — Dijkstra
|
||||
- [K 站中转内最便宜的航班](https://neetcode.io/problems/cheapest-flight-path) — 带约束的修改版 BFS/Bellman-Ford
|
||||
- [上升水温游泳](https://neetcode.io/problems/swim-in-rising-water) — 二分查找 + BFS 或网格上的 Dijkstra
|
||||
|
||||
### 进阶
|
||||
- [外星文字典](https://neetcode.io/problems/foreign-dictionary) — 从字符顺序进行拓扑排序
|
||||
Reference in New Issue
Block a user