在交通导航、网络路由、物流配送等场景中,我们经常需要找到两点之间的最优路径。这类问题在图论中被称为"最短路径问题"。1956年,荷兰计算机科学家Edsger Dijkstra提出了一种经典解决方案——Dijkstra算法,至今仍是解决非负权图单源最短路径问题的黄金标准。
Dijkstra算法的核心思想是贪心策略:从起点开始,逐步扩展到距离最近的未访问节点,通过松弛操作更新相邻节点的最短距离。这种"局部最优导致全局最优"的特性,使其时间复杂度可以达到O(V²)(使用邻接矩阵)或O(E + VlogV)(使用优先队列优化),其中V代表顶点数,E代表边数。
注意:Dijkstra算法仅适用于边权为非负数的图。若存在负权边,需要使用Bellman-Ford或SPFA等算法。
实现Dijkstra算法需要维护三个核心数据结构:
python复制# 初始化示例(Python)
dist = [float('inf')] * n # n为顶点数
dist[source] = 0
heap = [(0, source)]
visited = [False] * n
python复制while heap:
current_dist, u = heapq.heappop(heap)
if visited[u]:
continue
visited[u] = True
for v, weight in graph[u]:
if dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
heapq.heappush(heap, (dist[v], v))
松弛(Relaxation)是Dijkstra算法的核心操作,其数学表达式为:
code复制dist[v] = min(dist[v], dist[u] + weight(u,v))
这相当于在物理系统中寻找能量最低的状态。每次松弛都在尝试找到更短的路径,而算法保证每个节点在被标记为已访问时,其dist值就是最终的最短距离。
以下是使用邻接表的Python实现:
python复制import heapq
def dijkstra(graph, start):
n = len(graph)
dist = [float('inf')] * n
dist[start] = 0
heap = [(0, start)]
visited = set()
while heap:
current_dist, u = heapq.heappop(heap)
if u in visited:
continue
visited.add(u)
for v, weight in graph[u]:
if current_dist + weight < dist[v]:
dist[v] = current_dist + weight
heapq.heappush(heap, (dist[v], v))
return dist
不同语言中优先队列的实现方式会影响算法效率:
heapq模块(最小堆)PriorityQueue类priority_queue容器(需注意默认是最大堆)实际测试发现,在Python中使用
heapq处理大规模图时,可以考虑使用(distance, node)元组存储,但要注意当距离相同时比较node可能导致性能下降。
基础实现只计算最短距离,要获取完整路径需要额外维护前驱数组:
python复制def dijkstra_with_path(graph, start):
n = len(graph)
dist = [float('inf')] * n
prev = [-1] * n # 前驱节点数组
dist[start] = 0
heap = [(0, start)]
while heap:
current_dist, u = heapq.heappop(heap)
if current_dist > dist[u]:
continue
for v, weight in graph[u]:
if dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
prev[v] = u
heapq.heappush(heap, (dist[v], v))
return dist, prev
def reconstruct_path(prev, target):
path = []
while target != -1:
path.append(target)
target = prev[target]
return path[::-1]
| 实现方式 | 时间复杂度 | 适用场景 |
|---|---|---|
| 邻接矩阵+线性搜索 | O(V²) | 稠密图(边数接近V²) |
| 邻接表+二叉堆 | O((V+E)logV) | 稀疏图(边数远小于V²) |
| 斐波那契堆 | O(E + VlogV) | 理论最优,但常数较大 |
所有实现的空间复杂度都是O(V+E),主要用于存储图和辅助数据结构。
Dijkstra算法不能处理负权边,因为贪心策略会失效。例如下图中,算法会错误地认为A→B的最短距离是2,而实际上A→C→B的路径总权值为-1。
code复制A --2--> B
\ /
3 -4
\ /
C
解决方案:
dist[u] < current_dist检查跳过过时的记录abs(a-b) < epsilonpython复制# 提前终止示例
def dijkstra_to_target(graph, start, end):
# ...初始化部分相同...
while heap:
current_dist, u = heapq.heappop(heap)
if u == end: # 找到目标节点
return current_dist
# ...剩余部分相同...
return float('inf') # 不可达
当图中所有边权相等时(可视为1),Dijkstra算法退化为BFS:
| 特性 | Dijkstra | Floyd-Warshall |
|---|---|---|
| 类型 | 单源最短路径 | 全源最短路径 |
| 时间复杂度 | O((V+E)logV) | O(V³) |
| 空间复杂度 | O(V+E) | O(V²) |
| 适用图类型 | 非负权图 | 可处理负权边(无负环) |
| 实现难度 | 中等 | 较简单 |
A*算法是Dijkstra的启发式改进:
python复制def a_star(graph, start, end, heuristic):
# heuristic(v) 是从v到end的估计距离
open_set = [(0 + heuristic(start), 0, start)]
g_score = {start: 0}
while open_set:
_, g, current = heapq.heappop(open_set)
if current == end:
return g
for neighbor, weight in graph[current]:
tentative_g = g + weight
if tentative_g < g_score.get(neighbor, float('inf')):
g_score[neighbor] = tentative_g
heapq.heappush(open_set, (tentative_g + heuristic(neighbor), tentative_g, neighbor))
return float('inf')
当图规模极大时(如社交网络),标准Dijkstra可能遇到内存问题:
Dijkstra本质是顺序算法,但可以部分并行化:
cpp复制// C++中使用bitset示例
std::bitset<MAX_NODES> visited;
// 标记节点i为已访问
visited.set(i);
// 检查节点i是否已访问
if (visited.test(i)) { ... }
我在实际项目中发现,当图的规模超过100万个节点时,使用传统的邻接表实现会遇到性能瓶颈。这时切换到CSR格式存储,配合良好的缓存策略,可以将运行时间减少40%左右。另一个实用技巧是:在已知图结构相对静态的场景下,可以预计算并缓存常见查询的最短路径,牺牲一些内存换取查询速度的显著提升。