30 秒速览
- 什么时候用:关系网络、路径问题、连通性、依赖关系
- 核心操作:遍历、拓扑排序、并查集、最短路径
- 存储方式:邻接矩阵 vs 邻接表
- 遍历方式:DFS(递归/栈)、BFS(队列)
一、直觉建立
什么是图?
图由**顶点(Vertex)和边(Edge)**组成,表示事物之间的关系。
A --- B
| |
C --- D
顶点:A, B, C, D
边:(A,B), (A,C), (B,D), (C,D)
图的分类
| 类型 | 特点 | 例子 |
|---|---|---|
| 无向图 | 边无方向 | 社交网络 |
| 有向图 | 边有方向 | 网页链接 |
| 有权图 | 边有权重 | 地图导航 |
| 无权图 | 边无权重 | 连通性 |
图的存储
邻接矩阵:
# matrix[i][j] = 1 表示 i 和 j 相连
matrix = [
[0, 1, 1, 0], # A
[1, 0, 0, 1], # B
[1, 0, 0, 1], # C
[0, 1, 1, 0], # D
]
邻接表:
# graph[i] = [相邻节点列表]
graph = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'D'],
'D': ['B', 'C'],
}
| 方式 | 空间 | 适用场景 |
|---|---|---|
| 邻接矩阵 | O(V²) | 稠密图、频繁查边 |
| 邻接表 | O(V+E) | 稀疏图、遍历操作 |
二、图的遍历
DFS 模板
def dfs(graph, start, visited):
visited.add(start)
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
BFS 模板
from collections import deque
def bfs(graph, start):
queue = deque([start])
visited = set([start])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
标记时机的区别
# 方式 1:入队时标记(推荐)
queue.append(start)
visited.add(start)
# 方式 2:出队时标记(可能重复入队)
queue.append(start)
while queue:
node = queue.popleft()
visited.add(node) # 太晚了
三、典型例题
例题 1:LC 200. 岛屿数量
题目:给定一个由 ‘1’(陆地)和 ‘0’(水)组成的二维网格,计算岛屿数量。
思路
网格可以看作图,每个格子是顶点,上下左右是边。
DFS 实现:
def numIslands(grid: list[list[str]]) -> int:
if not grid:
return 0
m, n = len(grid), len(grid[0])
count = 0
def dfs(i, j):
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] == '0':
return
grid[i][j] = '0' # 标记为已访问
dfs(i + 1, j)
dfs(i - 1, j)
dfs(i, j + 1)
dfs(i, j - 1)
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
count += 1
dfs(i, j)
return count
复杂度:时间 O(mn),空间 O(mn)(递归栈)
例题 2:LC 207. 课程表
题目:给定课程数量和先修关系,判断是否能完成所有课程。
示例:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true(先修课程 0,再修课程 1)
拓扑排序
这是经典的拓扑排序问题,本质是检测有向图是否有环。
BFS(Kahn 算法):
from collections import deque, defaultdict
def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
# 构建邻接表和入度数组
graph = defaultdict(list)
in_degree = [0] * numCourses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
# 入度为 0 的课程入队
queue = deque([i for i in range(numCourses) if in_degree[i] == 0])
count = 0
while queue:
course = queue.popleft()
count += 1
for neighbor in graph[course]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return count == numCourses
复杂度:时间 O(V+E),空间 O(V+E)
DFS 检测环:
def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
graph = defaultdict(list)
for course, prereq in prerequisites:
graph[prereq].append(course)
# 0: 未访问, 1: 访问中, 2: 已完成
state = [0] * numCourses
def has_cycle(course):
if state[course] == 1: # 发现环
return True
if state[course] == 2: # 已处理
return False
state[course] = 1 # 标记为访问中
for neighbor in graph[course]:
if has_cycle(neighbor):
return True
state[course] = 2 # 标记为已完成
return False
for i in range(numCourses):
if has_cycle(i):
return False
return True
例题 3:LC 210. 课程表 II
题目:返回完成所有课程的学习顺序。
from collections import deque, defaultdict
def findOrder(numCourses: int, prerequisites: list[list[int]]) -> list[int]:
graph = defaultdict(list)
in_degree = [0] * numCourses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque([i for i in range(numCourses) if in_degree[i] == 0])
result = []
while queue:
course = queue.popleft()
result.append(course)
for neighbor in graph[course]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result if len(result) == numCourses else []
例题 4:LC 547. 省份数量
题目:给定 n 个城市的连接矩阵,返回省份数量(连通分量数)。
def findCircleNum(isConnected: list[list[int]]) -> int:
n = len(isConnected)
visited = [False] * n
count = 0
def dfs(city):
visited[city] = True
for j in range(n):
if isConnected[city][j] == 1 and not visited[j]:
dfs(j)
for i in range(n):
if not visited[i]:
count += 1
dfs(i)
return count
例题 5:LC 684. 冗余连接
题目:在无向图中找出一条可以删除的边,使图成为树。
并查集
并查集用于处理连通性问题,支持两种操作:
find(x):找 x 的根union(x, y):合并 x 和 y 所在的集合
def findRedundantConnection(edges: list[list[int]]) -> list[int]:
parent = list(range(len(edges) + 1))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # 路径压缩
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px == py:
return False # 已经连通
parent[px] = py
return True
for x, y in edges:
if not union(x, y):
return [x, y]
return []
并查集优化:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n # 按秩合并
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
# 按秩合并
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
例题 6:LC 994. 腐烂的橘子
题目:网格中的橘子,每分钟腐烂的橘子会感染相邻的新鲜橘子,求所有橘子腐烂的最少分钟数。
多源 BFS
from collections import deque
def orangesRotting(grid: list[list[int]]) -> int:
m, n = len(grid), len(grid[0])
queue = deque()
fresh = 0
# 初始化:所有腐烂橘子入队
for i in range(m):
for j in range(n):
if grid[i][j] == 2:
queue.append((i, j))
elif grid[i][j] == 1:
fresh += 1
if fresh == 0:
return 0
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
minutes = 0
while queue and fresh > 0:
minutes += 1
for _ in range(len(queue)):
x, y = queue.popleft()
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == 1:
grid[nx][ny] = 2
fresh -= 1
queue.append((nx, ny))
return minutes if fresh == 0 else -1
复杂度:时间 O(mn),空间 O(mn)
四、图问题分类
| 问题类型 | 核心算法 | 典型题目 |
|---|---|---|
| 连通性 | DFS/BFS/并查集 | 岛屿数量、省份数量 |
| 拓扑排序 | Kahn/DFS | 课程表 |
| 环检测 | DFS 三色/并查集 | 课程表、冗余连接 |
| 最短路径 | BFS/Dijkstra | 单词接龙 |
| 最小生成树 | Kruskal/Prim | 连接所有点 |
五、常见错误与陷阱
错误 1:忘记标记已访问
# 错误:死循环
def dfs(node):
for neighbor in graph[node]:
dfs(neighbor) # 无限递归
# 正确
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
错误 2:BFS 标记时机
# 错误:出队时标记,可能重复入队
while queue:
node = queue.popleft()
visited.add(node) # 太晚
# 正确:入队时标记
visited.add(start)
queue.append(start)
while queue:
...
错误 3:并查集忘记路径压缩
# 未优化:可能退化成链表
def find(x):
while parent[x] != x:
x = parent[x]
return x
# 优化:路径压缩
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
错误 4:有向图环检测状态错误
# 三种状态
# 0: 未访问
# 1: 正在访问(当前路径上)
# 2: 已完成
# 只有状态 1 -> 1 才是环
六、复盘清单
触发信号
- 关系网络
- 连通性判断
- 依赖关系/先修条件
- 最短路径
- 环检测
算法选择
- 连通分量 → DFS/BFS/并查集
- 依赖关系 → 拓扑排序
- 动态连通性 → 并查集
- 最短路径(无权)→ BFS
- 最短路径(有权)→ Dijkstra
边界条件
- 空图
- 单节点
- 全连通
- 有环/无环
七、题目清单
基础题
| 题号 | 题目 | 难度 | 考点 |
|---|---|---|---|
| 200 | 岛屿数量 | Medium | DFS/BFS |
| 547 | 省份数量 | Medium | DFS/并查集 |
| 207 | 课程表 | Medium | 拓扑排序 |
| 994 | 腐烂的橘子 | Medium | 多源 BFS |
进阶题
| 题号 | 题目 | 难度 | 考点 |
|---|---|---|---|
| 210 | 课程表 II | Medium | 拓扑排序 |
| 684 | 冗余连接 | Medium | 并查集 |
| 133 | 克隆图 | Medium | 图复制 |
| 127 | 单词接龙 | Hard | BFS |
| 743 | 网络延迟时间 | Medium | Dijkstra |