1. 图论基础概念回顾
图论作为离散数学的重要分支,在计算机科学领域有着广泛的应用。在进入更深入的图论知识之前,我们先快速回顾几个核心概念:
- 图(Graph):由顶点(Vertex)和边(Edge)组成的结构,记作G=(V,E)
- 有向图与无向图:边是否有方向性
- 权重图:边上带有权值的图
- 度(Degree):与顶点相连的边数
- 路径与环:顶点序列与闭合路径
理解这些基础概念是掌握后续高级内容的前提。在实际编程中,我们通常使用邻接矩阵或邻接表来表示图结构,两者各有优劣:
python复制# 邻接矩阵表示法
graph_matrix = [
[0, 1, 1, 0],
[1, 0, 1, 1],
[1, 1, 0, 0],
[0, 1, 0, 0]
]
# 邻接表表示法
graph_list = {
'A': ['B', 'C'],
'B': ['A', 'C', 'D'],
'C': ['A', 'B'],
'D': ['B']
}
提示:邻接矩阵适合稠密图,邻接表适合稀疏图。实际应用中要根据图的特性选择合适的数据结构。
2. 图的遍历算法进阶
2.1 深度优先搜索(DFS)的优化技巧
深度优先搜索是图论中最基础的算法之一,但实际应用中我们常常需要对其进行优化:
- 迭代式DFS实现:避免递归导致的栈溢出
python复制def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
vertex = stack.pop()
if vertex not in visited:
visited.add(vertex)
stack.extend(reversed(graph[vertex])) # 保持顺序一致
return visited
-
双色标记法:在遍历时区分"已访问"和"正在访问"状态,可用于检测环
-
时间戳记录:记录每个节点的发现时间和完成时间,在拓扑排序等应用中非常有用
注意事项:DFS的空间复杂度为O(h),其中h是图的最大深度。对于极深图结构,迭代实现更安全。
2.2 广度优先搜索(BFS)的应用变种
广度优先搜索在最短路径问题中表现优异,以下是几种实用变体:
- 双向BFS:从起点和终点同时开始搜索,适用于已知起点和终点的场景
python复制def bidirectional_bfs(graph, start, end):
if start == end:
return [start]
# 初始化两个队列和已访问集合
queue_start = deque([start])
queue_end = deque([end])
visited_start = {start: None}
visited_end = {end: None}
while queue_start and queue_end:
# 从起点开始的BFS步骤
current_start = queue_start.popleft()
for neighbor in graph[current_start]:
if neighbor in visited_end:
# 构建路径
path = [neighbor]
node = current_start
while node is not None:
path.append(node)
node = visited_start[node]
path.reverse()
node = visited_end[neighbor]
while node is not None:
path.append(node)
node = visited_end[node]
return path
if neighbor not in visited_start:
visited_start[neighbor] = current_start
queue_start.append(neighbor)
# 从终点开始的BFS步骤(类似上面)
# ...
-
多源BFS:从多个起点同时开始BFS,适用于如"多个污染源扩散"类问题
-
A*搜索:结合启发式函数的BFS变种,在路径规划中广泛应用
3. 最短路径算法实战
3.1 Dijkstra算法精讲
Dijkstra算法解决的是带权图的单源最短路径问题,其核心思想是贪心算法:
python复制import heapq
def dijkstra(graph, start):
distances = {vertex: float('infinity') for vertex in graph}
distances[start] = 0
pq = [(0, start)]
while pq:
current_distance, current_vertex = heapq.heappop(pq)
if current_distance > distances[current_vertex]:
continue
for neighbor, weight in graph[current_vertex].items():
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
时间复杂度分析:
- 普通实现:O(V²)
- 优先队列优化:O(E + VlogV)
实操心得:Dijkstra不能处理负权边,遇到负权边应考虑Bellman-Ford算法。在实际编码中,优先队列的实现方式会显著影响性能。
3.2 Floyd-Warshall全源最短路径
Floyd-Warshall算法通过动态规划解决所有顶点对之间的最短路径问题:
python复制def floyd_warshall(graph):
dist = {u: {v: float('infinity') for v in graph} for u in graph}
for u in graph:
dist[u][u] = 0
for v, w in graph[u].items():
dist[u][v] = w
for k in graph:
for i in graph:
for j in graph:
if dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
应用场景:
- 网络路由优化
- 交通路径规划
- 社交网络中的关系距离计算
4. 最小生成树算法
4.1 Kruskal算法实现
Kruskal算法基于并查集数据结构,按边权值从小到大选择不形成环的边:
python复制class UnionFind:
def __init__(self, size):
self.parent = list(range(size))
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
if x_root != y_root:
self.parent[y_root] = x_root
def kruskal(vertices, edges):
uf = UnionFind(len(vertices))
edges.sort(key=lambda x: x[2])
mst = []
for edge in edges:
u, v, weight = edge
if uf.find(u) != uf.find(v):
uf.union(u, v)
mst.append(edge)
if len(mst) == len(vertices) - 1:
break
return mst
4.2 Prim算法的优化实现
Prim算法从一个顶点开始,逐步扩展最小生成树:
python复制import heapq
def prim(graph, start):
mst = []
visited = set([start])
edges = [
(weight, start, to)
for to, weight in graph[start].items()
]
heapq.heapify(edges)
while edges:
weight, u, v = heapq.heappop(edges)
if v not in visited:
visited.add(v)
mst.append((u, v, weight))
for neighbor, w in graph[v].items():
if neighbor not in visited:
heapq.heappush(edges, (w, v, neighbor))
return mst
算法选择建议:
- 稠密图:Prim算法更优
- 稀疏图:Kruskal算法更合适
5. 拓扑排序与关键路径
5.1 拓扑排序的Kahn算法
Kahn算法通过不断移除入度为0的顶点来实现拓扑排序:
python复制def topological_sort_kahn(graph):
in_degree = {u: 0 for u in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
queue = deque([u for u in graph if in_degree[u] == 0])
topo_order = []
while queue:
u = queue.popleft()
topo_order.append(u)
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
if len(topo_order) != len(graph):
return None # 存在环
return topo_order
5.2 关键路径算法
关键路径用于计算项目计划中的最长路径(决定项目总工期):
python复制def critical_path(activities, dependencies):
# 构建图
graph = {act: [] for act in activities}
reverse_graph = {act: [] for act in activities}
in_degree = {act: 0 for act in activities}
for u, v, duration in dependencies:
graph[u].append((v, duration))
reverse_graph[v].append((u, duration))
in_degree[v] += 1
# 拓扑排序
topo_order = []
queue = deque([act for act in activities if in_degree[act] == 0])
while queue:
u = queue.popleft()
topo_order.append(u)
for v, _ in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
# 计算最早开始时间
earliest = {act: 0 for act in activities}
for u in topo_order:
for v, duration in graph[u]:
if earliest[v] < earliest[u] + duration:
earliest[v] = earliest[u] + duration
# 计算最晚开始时间
latest = {act: earliest[topo_order[-1]] for act in activities}
for u in reversed(topo_order):
for v, duration in graph[u]:
if latest[u] > latest[v] - duration:
latest[u] = latest[v] - duration
# 确定关键路径
critical = []
for u in topo_order:
for v, duration in graph[u]:
if earliest[u] == latest[v] - duration:
critical.append((u, v, duration))
return critical
6. 图论高级应用
6.1 最大流问题与Ford-Fulkerson算法
Ford-Fulkerson算法通过不断寻找增广路径来计算网络中的最大流:
python复制def ford_fulkerson(graph, source, sink):
# 创建剩余图
residual = {u: {} for u in graph}
for u in graph:
for v, cap in graph[u].items():
residual[u][v] = cap
residual[v][u] = 0
max_flow = 0
parent = {}
def bfs():
visited = set()
queue = deque([source])
visited.add(source)
while queue:
u = queue.popleft()
for v in residual[u]:
if v not in visited and residual[u][v] > 0:
visited.add(v)
parent[v] = u
if v == sink:
return True
queue.append(v)
return False
while bfs():
path_flow = float('infinity')
v = sink
while v != source:
u = parent[v]
path_flow = min(path_flow, residual[u][v])
v = u
v = sink
while v != source:
u = parent[v]
residual[u][v] -= path_flow
residual[v][u] += path_flow
v = u
max_flow += path_flow
return max_flow
6.2 二分图匹配
匈牙利算法解决二分图的最大匹配问题:
python复制def hungarian_algorithm(graph):
def bpm(u, seen, match_to):
for v in graph[u]:
if not seen[v]:
seen[v] = True
if match_to[v] == -1 or bpm(match_to[v], seen, match_to):
match_to[v] = u
return True
return False
# 初始化
match_to = {v: -1 for v in set().union(*graph.values())}
result = 0
for u in graph:
seen = {v: False for v in match_to}
if bpm(u, seen, match_to):
result += 1
return result, match_to
7. 图论问题实战技巧
7.1 常见图论问题模式识别
- 连通性问题:使用DFS/BFS/Union-Find
- 路径问题:Dijkstra/Bellman-Ford/Floyd-Warshall
- 树形问题:DFS/中心点分解/树形DP
- 匹配问题:匈牙利算法/最大流
- 网络流问题:Ford-Fulkerson/Dinic
7.2 图论算法优化策略
-
预处理技巧:
- 度数排序
- 邻接表优化
- 反向图构建
-
剪枝策略:
- 可行性剪枝
- 最优性剪枝
- 对称性剪枝
-
记忆化技术:
- 状态压缩
- 结果缓存
- 中间值复用
实战经验:在解决复杂图论问题时,先明确问题类型,再选择合适的算法框架,最后考虑优化策略。不要过早优化,正确性永远是第一位的。