⚡ 最短路径算法
解决图中两点间权重和最小的路径问题。主要包括Dijkstra(单源,非负权)、Floyd-Warshall(多源,全对最短路径)以及Bellman-Ford(处理负权边)。
从最短路径到网络流,从最小生成树到复杂网络分析。本页面提供深度的图论算法解析、实战代码示例及行业应用指南,帮助开发者与研究人员构建扎实的算法基础。
图(Graph)是数学和计算机科学中重要的数据结构,由顶点(Vertex)和边(Edge)组成。图论算法理论实现及应用涵盖了多种经典算法,用于解决连通性、路径规划、流量优化等问题。以下是必须掌握的核心算法类别:
解决图中两点间权重和最小的路径问题。主要包括Dijkstra(单源,非负权)、Floyd-Warshall(多源,全对最短路径)以及Bellman-Ford(处理负权边)。
在连通加权无向图中,寻找一棵包含所有顶点且边权总和最小的树。Prim算法适合稠密图,Kruskal算法适合稀疏图,两者均基于贪心策略。
针对有向无环图(DAG),将顶点排列为线性序列,使得对于每条有向边(u,v),u在v之前出现。广泛应用于任务调度、编译依赖分析等场景。
包括深度优先搜索 (DFS)和广度优先搜索 (BFS),以及用于寻找无向图割点、桥,有向图强连通分量(Tarjan算法)的高级技术。
| 算法名称 | 适用场景 | 时间复杂度 (邻接表) | 空间复杂度 | 特点 |
|---|---|---|---|---|
| Dijkstra | 单源最短路径,无负权 | O((V+E)logV) | O(V) | 贪心策略,高效稳定 |
| Floyd | 多源最短路径,稠密图 | O(V³) | O(V²) | 代码简洁,适合小规模图 |
| Prim | 最小生成树,稠密图 | O(ElogV) | O(V) | 加点法,类似Dijkstra |
| Kruskal | 最小生成树,稀疏图 | O(ElogE) | O(E) | 加边法,需使用并查集 |
| Bellman-Ford | 含负权边,检测负环 | O(VE) | O(V) | 可处理负权,较慢 |
在实际工程开发中,图论算法理论实现及应用往往不是单一的算法调用,而是多种技术的组合。以下是几个典型的复杂应用场景:
网络流主要用于解决资源在网络中的传输问题。核心概念包括最大流(Max Flow)和最小割(Min Cut)。
应用场景:二分图匹配、图像分割、任务分配、管道输油效率优化等。
随着深度学习的发展,图论算法与AI结合产生了图神经网络。GNN能够处理非欧几里得数据,直接从图结构中学习节点表示。
应用场景:分子性质预测、社交网络推荐、知识图谱补全、欺诈检测。
在地图导航系统中,单纯的Dijkstra往往不够用。工业级路径规划通常采用混合策略:
理论结合实践是掌握图论算法的关键。以下提供Python语言实现的经典Dijkstra算法示例,以及C++语言实现的并查集(用于Kruskal算法)片段。
import heapq
def dijkstra(graph, start):
"""
使用优先队列(最小堆)实现Dijkstra算法
:param graph: 邻接表形式的图,例如 {1: {2: 1, 3: 4}, 2: {3: 2}, 3: {}}
:param start: 起始节点
:return: 最短距离字典
"""
# 初始化距离,所有节点设为无穷大
distances = {node: float('inf') for node in graph}
distances[start] = 0
# 优先队列,存储 (距离, 节点)
pq = [(0, start)]
while pq:
current_dist, current_node = heapq.heappop(pq)
# 如果当前距离大于记录的最短距离,跳过
if current_dist > distances[current_node]:
continue
# 遍历邻居
for neighbor, weight in graph[current_node].items():
distance = current_dist + weight
# 松弛操作
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
示例用法
graph = {
'A': {'B': 1, 'C': 4},
'B': {'C': 2, 'D': 5},
'C': {'D': 1},
'D': {}
}
print(dijkstra(graph, 'A'))
输出: {'A': 0, 'B': 1, 'C': 3, 'D': 4}
#include <vector>
#include <numeric>
class UnionFind {
public:
std::vector<int> parent;
UnionFind(int n) {
parent.resize(n);
std::iota(parent.begin(), parent.end(), 0);
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
void unite(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
parent[rootX] = rootY; // 简单合并,未使用按秩合并
}
}
};欧拉(Leonhard Euler)解决了该问题,发表了第一篇图论论文,标志着图论的诞生。他证明了不存在一条路径能经过每座桥恰好一次。
威廉·罗文·汉密尔顿提出了哈密顿回路问题,即寻找一条经过图中每个顶点恰好一次的路径,这是NP完全问题的早期代表。
艾兹格·迪科斯彻(Edsger W. Dijkstra)提出了Dijkstra算法,用于解决单源最短路径问题,成为计算机科学中最著名的算法之一。
随着大数据和AI的兴起,图算法扩展到十亿级节点规模(如Pregel, GraphX),图神经网络(GNN)成为处理图结构数据的主流技术。
A: Dijkstra算法基于贪心策略,一旦节点被标记为“已访问”(即找到了最短路径),就不会再更新。如果存在负权边,后续可能会通过负权边找到更短的路径,从而破坏贪心选择的正确性。此时应使用Bellman-Ford算法或SPFA算法。
A: 没有绝对的优劣,取决于图的稠密程度。Prim算法的时间复杂度主要取决于顶点数V和边数E,通常表示为O(ElogV)或O(V²),适合边数较多的稠密图。Kruskal算法的时间复杂度主要取决于边数E,为O(ElogE),适合边数较少的稀疏图。在实际编程竞赛中,Kruskal由于代码简洁(只需排序边+并查集)而更常用。
A: 对于无向图,可以使用并查集或DFS/BFS检测回边。如果在遍历过程中遇到一个已访问过的邻居节点(且不是当前节点的父亲),则存在环。
对于有向图
可以使用DFS检测回溯边,或者使用拓扑排序。如果拓扑排序后输出的节点数少于图中总节点数,则说明存在环。
A: 高频考点包括:
1. 实现BFS/DFS及其变种(如层次遍历)。
2. 最短路径问题(Dijkstra, A)的实现与优化。
3. 并查集的实现与应用(如连通分量计数、环检测)。
4. 拓扑排序(课程表问题)。
5. 二分图判定(染色法)。
6. 最小生成树(Prim/Kruskal)的代码编写。