图
核心理念:由顶点(Vertex)和边(Edge)组成的网络结构。
为何重要:用于表示各种网络关系,如社交网络、地图路线、依赖关系等。
图有邻接表和邻接矩阵两种形式,算法中通常用邻接表。
必练操作:
- 图的表示方法:邻接矩阵和邻接表。
- 图的遍历:深度优先搜索(DFS)和广度优先搜索(BFS)。
- (进阶)最小生成树算法 (Prim, Kruskal)、最短路径算法 (Dijkstra)。
图的遍历框架
图的遍历与树的遍历最大的区别在于:图可能存在环,因此需要使用 visited 数组来避免重复访问节点。
图的表示(邻接表)
| # 邻接表表示图
graph: list[list[int]] # graph[i] 存储节点 i 的所有邻居节点
|
DFS vs BFS
DFS 使用递归或栈来实现,适合解决路径、连通性、环检测等问题。
| def dfs(graph: list[list[int]], start: int, visited: set[int]) -> None:
# 标记当前节点已访问
visited.add(start)
# 前序位置:进入节点时的操作
# ...
# 遍历所有邻居节点
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
# 后序位置:离开节点时的操作
# ...
def traverse_graph(graph: list[list[int]]) -> None:
visited = set()
# 遍历所有节点,确保访问到所有连通分量
for i in range(len(graph)):
if i not in visited:
dfs(graph, i, visited)
|
BFS 使用队列来实现,适合解决最短路径、层级遍历等问题。
| from collections import deque
def bfs(graph: list[list[int]], start: int) -> None:
visited = set()
queue = deque([start])
visited.add(start)
while queue:
# 取出队首节点
node = queue.popleft()
# 访问当前节点
# ...
# 将所有未访问的邻居加入队列
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
|
| 特性 |
DFS |
BFS |
| 数据结构 |
栈(递归) |
队列 |
| 空间复杂度 |
\(O(h)\) (h 为图的深度) |
\(O(w)\) (w 为图的宽度) |
| 适用场景 |
路径问题、拓扑排序、环检测 |
最短路径、层级遍历 |
| 遍历顺序 |
深度优先,一条路走到底 |
广度优先,逐层扩散 |
常见应用场景
| DFS 应用 |
BFS 应用 |
| 检测图中是否有环 |
无权图的最短路径 |
| 拓扑排序 |
层级遍历 |
| 寻找所有路径 |
最小步数问题 |
| 连通分量计数 |
二分图检测 |
关键注意事项
- visited 数组: 防止重复访问,避免死循环
- 非连通图: 需要遍历所有节点作为起点,确保访问到所有连通分量
- 有向图 vs 无向图: 邻接表的构建方式不同
- 无向图:
graph[u] 包含 v,graph[v] 也包含 u
- 有向图: 只在
graph[u] 中添加 v
图的 DFS vs 回溯 DFS
图的 DFS 和回溯算法都使用深度优先搜索,但它们的目的和实现细节有重要区别。
核心区别
| 维度 |
图的 DFS (遍历) |
回溯 (找所有路径) |
| 目的 |
遍历每个节点一次 |
找所有可能的路径/组合 |
| visited 含义 |
"这个节点被访问过了" |
"这个节点在当前路径中" |
| 做选择 |
✅ for 循环外 |
✅ for 循环外 |
| 撤销选择 |
❌ 不撤销 |
✅ for 循环后撤销 |
| 节点重复访问 |
❌ 每个节点只访问一次 |
✅ 不同路径可以重复访问同一节点 |
代码对比
目的: 遍历每个节点一次
| def graph_dfs(graph: list[list[int]], start: int, visited: set[int]) -> None:
# 做选择(for 循环外)
visited.add(start)
# 前序位置:访问节点
print(start)
# 遍历所有邻居
for neighbor in graph[start]:
if neighbor not in visited:
graph_dfs(graph, neighbor, visited)
# ✖️ 不撤销选择
|
目的: 找所有可能的路径/组合
| def backtrack_dfs(
graph: list[list[int]],
start: int,
target: int,
visited: set[int],
path: list[int],
result: list[list[int]]
) -> None:
# 做选择(for 循环外)
path.append(start)
visited.add(start)
# 到达目标,记录路径
if start == target:
result.append(path.copy())
# 遍历所有邻居
for neighbor in graph[start]:
if neighbor not in visited:
backtrack_dfs(graph, neighbor, target, visited, path, result)
# ✅ 撤销选择(for 循环外) - 这是回溯的精髓!
visited.remove(start)
path.pop()
|
具体例子
假设有这样一个图:
记忆要点
回溯 = DFS + 撤销选择
图的 DFS 关注"是否访问过",回溯关注"当前路径的状态"。撤销选择是回溯的精髓,它允许算法探索所有可能的解空间。
二分图
二分图是一种特殊的图,其顶点可以分成两个互不相交的集合,使得 同一集合内的顶点之间没有边。
二分图判定
使用 染色法(DFS/BFS):尝试用两种颜色给图着色,如果能成功着色且相邻节点颜色不同,则是二分图。
| def is_bipartite(graph: list[list[int]]) -> bool:
n = len(graph)
color = [0] * n # 0: 未染色, 1: 颜色1, -1: 颜色2
def dfs(node: int, c: int) -> bool:
color[node] = c
for neighbor in graph[node]:
if color[neighbor] == c:
# 相邻节点颜色相同,不是二分图
return False
if color[neighbor] == 0:
# 染成相反的颜色
if not dfs(neighbor, -c):
return False
return True
# 处理非连通图
for i in range(n):
if color[i] == 0:
if not dfs(i, 1):
return False
return True
|
应用场景:
环检测
拓扑排序
拓扑排序是对 有向无环图(DAG,Directed Acyclic Graph) 的所有顶点进行线性排序,使得对于任何有向边 u → v,u 在排序中都出现在 v 之前。简而言之,拓扑排序就是将依赖关系排成一条线。
拓扑排序有两种经典实现:Kahn 算法(BFS + 入度)和 DFS 算法(后序遍历反转)。两者时间复杂度相同 \(O(V+E)\),本质等价,不分优劣。

DFS + 后序遍历反转
|

Kahn 算法 (BFS + 入度)
|
图片来源: Jingsam
应用场景:
无权图
无权图的边没有权重,只表示节点之间的连接关系。这是最基础的图结构,适合表示二元关系。
无权图的表示
| # 邻接表表示无权图
graph: list[list[int]] # graph[i] 存储节点 i 的所有邻居
# 邻接矩阵表示无权图
matrix: list[list[bool]] # matrix[i][j] = True 表示 i 和 j 之间有边
|
BFS 求最短路径
在 无权图 中,BFS 可以找到从起点到任意节点的 最短路径(边数最少)。
| from collections import deque
def shortest_path(graph: list[list[int]], start: int, target: int) -> int:
visited = set()
queue = deque([(start, 0)]) # (node, step)
visited.add(start)
while queue:
node, step = queue.popleft()
if node == target:
return step
# 将邻居加入队列
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, step + 1))
return -1 # 无法到达
|
双向 BFS 优化
当起点和终点都已知时,可以使用 双向 BFS 来优化搜索效率。
| def bidirectional_bfs(graph: list[list[int]], start: int, target: int) -> int:
if start == target:
return 0
# 从起点和终点同时开始搜索
visited_start = {start}
visited_target = {target}
queue_start = deque([start])
queue_target = deque([target])
step = 0
while queue_start and queue_target:
step += 1
# 优化:总是扩展较小的队列
if len(queue_start) > len(queue_target):
queue_start, queue_target = queue_target, queue_start
visited_start, visited_target = visited_target, visited_start
for _ in range(len(queue_start)):
node = queue_start.popleft()
for neighbor in graph[node]:
if neighbor in visited_target:
# 两个方向相遇
return step
if neighbor not in visited_start:
visited_start.add(neighbor)
queue_start.append(neighbor)
return -1
|
双向 BFS 的优势:
- 时间复杂度从 \(O(b^d)\) 降低到 \(O(b^{d/2})\) (b 为分支因子,d 为深度)
- 适合搜索空间很大的场景
应用场景
无权图的典型应用:
- 社交网络 - 好友关系、关注关系
- 好友推荐(共同好友)
- 六度分离理论
- 影响力传播
- 网络拓扑 - 计算机网络、互联网连接
- 迷宫/棋盘 - 最短路径问题
- 单词接龙 - 词汇转换问题
- 基因序列 - DNA 序列相似度
经典题目
加权图
加权图的边带有权重,常用于最短路径、最小生成树等问题。
加权图的表示
| from typing import NamedTuple
class Edge(NamedTuple):
to: int
weight: int
# 方法 1: 邻接表 + Edge 类
graph: list[list[Edge]]
# 方法 2: 邻接表 + 元组
# graph[i] = [(neighbor1, weight1), (neighbor2, weight2), ...]
graph: list[list[tuple[int, int]]]
|
Dijkstra 算法(单源最短路径)
适用于 非负权重 的图,使用优先队列(最小堆)。
| import heapq
from typing import NamedTuple
class Edge(NamedTuple):
to: int
weight: int
def dijkstra(graph: list[list[Edge]], start: int) -> list[int]:
n = len(graph)
dist = [float('inf')] * n
dist[start] = 0
# 优先队列: (distance, node)
pq = [(0, start)]
while pq:
d, node = heapq.heappop(pq)
if d > dist[node]:
continue
for edge in graph[node]:
new_dist = dist[node] + edge.weight
if new_dist < dist[edge.to]:
dist[edge.to] = new_dist
heapq.heappush(pq, (new_dist, edge.to))
return dist
|
Bellman-Ford 算法
适用于 有负权重 的图,可以检测负权环。
| def bellman_ford(edges: list[tuple[int, int, int]], n: int, start: int) -> list[int]:
# edges[i] = (from, to, weight)
dist = [float('inf')] * n
dist[start] = 0
# 松弛 n-1 次
for _ in range(n - 1):
for from_node, to_node, weight in edges:
if dist[from_node] != float('inf') and dist[from_node] + weight < dist[to_node]:
dist[to_node] = dist[from_node] + weight
# 检测负权环
for from_node, to_node, weight in edges:
if dist[from_node] != float('inf') and dist[from_node] + weight < dist[to_node]:
# 存在负权环
return []
return dist
|
最短路径算法对比
| 算法 |
适用场景 |
时间复杂度 |
空间复杂度 |
| Dijkstra |
非负权重,单源最短路径 |
\(O((V+E) \log V)\) |
\(O(V)\) |
| Bellman-Ford |
有负权重,单源最短路径,检测负权环 |
\(O(VE)\) |
\(O(V)\) |
| Floyd-Warshall |
所有节点对最短路径 |
\(O(V^3)\) |
\(O(V^2)\) |
应用场景: