假设你打开地图,准备从学校前往一家从未去过的书店。地图上有无数条道路:有的距离短,却拥堵严重;有的看似绕远,实际耗时更少。导航软件必须从这些可能的路线中,找出一条总代价最小的路径。
如果道路只有十几条,我们或许可以逐一尝试。但当一张地图包含数百万个路口和道路时,穷举所有路线几乎不可能。我们需要一种更聪明的方法:也就是 Dijkstra 算法。
Table of contents
Open Table of contents
Origin
从鹿特丹到格罗宁根的最短路径是什么?实际上,这就是对于任意两座城市之间的最短路问题。解决这个问题实际上大概只花了我 20 分钟:一天早上,我和我的未婚妻在阿姆斯特丹购物,累了,我们便坐在咖啡馆的露台上喝咖啡,然后我就试了一下能否用一个算法解决最短路问题。正如我所说,这是一个 20 分钟的发现。不过实际上,我在 3 年后的 1959 年才把这个算法发表在论文上。即使现在来看这篇论文的可读性也非常高,这个算法之所以如此优雅,其中一个原因就是我没用笔纸就设计了它。后来我才知道,没用笔纸设计的优点之一是你不得不避免所有可避免的复杂问题。令我惊讶的是,这个算法最终成为我成名的基石之一。
— Edsger Dijkstra, in an interview with Philip L. Frana, Communications of the ACM, 2001

Question:非负权有向图上的单源最短路径
设有一个带权有向图 。其中:
- 是节点集合;
- 是有向边集合;
- 每条边 有权重 。
权重可以表示距离、时间或费用。 表示所有边权都非负。给定源点 ,单源最短路径要计算 到每个可达节点 的最小路径长度,记为 。
例如,边 的权重为 ,表示从 直接到 的代价是 。图是有向的,因此 不自动意味着也能从 到 。
从「暂定距离」开始
算法维护数组 distances。distances[v] 是目前已知的从源点到 的最好距离,因此它是一个上界:它可能还不是最短距离,但绝不会小于真正的最短距离。为方便证明,下文记 ,它表示节点 当前的暂定距离。
初始化时:
- 源点 的暂定距离为 ;
- 其他节点的暂定距离为 ;
- 所有节点都还没有被确定。
当已经知道到 的一条路径时,我们检查每条出边 。如果经过 到 更短,就更新 的暂定距离:
这个更新动作叫作松弛(relaxation)。它的含义很朴素:已有路线 加上最后一条边 ,也许能改进到 的最佳已知路线。
贪心选择为什么正确
算法反复从尚未确定的节点中,选出暂定距离最小的一个。关键不变量是:
每当选出暂定距离最小的未确定节点时,它的暂定距离就已经是最终最短距离。
归纳假设
把已经确定最短距离的节点组成的集合记为 ,其余节点记为 。本轮从 中选出暂定距离最小、且暂定距离有限的节点 。
我们按节点加入 的顺序归纳。开始时,源点 以 加入 ,结论成立。
现在假设 中每个节点 都满足 ,证明新选出的 也满足这个等式。
反证:假设 还能更近
每个有限的 都来自一条实际发现的 到 的路径,所以 。若结论不成立,只可能是 。取一条从 到 的真实最短路径 。
因为 而 ,沿着 前进时必然会第一次从 走到 。设跨越边界的边为 :

这里, 是 上最后一个属于 的节点, 是紧接着第一个属于 的节点。
关键不等式
-
的距离已经正确。 归纳假设给出:
-
算法已经处理过 。 当 进入 时,算法松弛过这条边;之后 只可能变小,因此:
-
右侧正好是路径 到 的前缀长度。 的 到 前缀本身也是最短路径;否则用一条更短的前缀替换它,就能得到一条比 更短的 到 路径。于是:
-
非负边权使前缀不会更长。 从 到 的后缀边权都非负,后缀长度不小于 ,所以:
由第 3 步和第 4 步可得 。但 ,而 是从 中选出的暂定距离最小的节点,矛盾。
因此, 不成立;又总有 ,所以 。这完成了归纳步骤。若剩余节点的暂定距离全为 ,它们从 不可达,最短距离也正是 。
为什么负权边会破坏证明
证明唯一使用「边权非负」的地方是第 4 步:后缀长度必须不小于 。若存在负权边,路径前缀可能比整条路径更长,便无法推出 ;算法也就不能保证当前选出的节点不会在以后变得更近。这正是 Dijkstra 算法不适用于负权边的原因。
由不变量推导算法
证明告诉我们可以安全地重复以下过程:
- 取出暂定距离最小的未确定节点 ;
- 将 标记为距离已确定;
- 松弛 的所有出边;
- 直到没有可处理的节点。
为了让代码更容易理解,下面的实现每轮都用普通循环寻找暂定距离最小的未确定节点。
Implementation
为了方便理解,我们可以看这张图:

def dijkstra(graph, source):
distances = {}
visited = set()
for node in graph:
distances[node] = float("inf")
distances[source] = 0
while True:
current_node = None
current_distance = float("inf")
for node in graph:
if node not in visited and distances[node] < current_distance:
current_node = node
current_distance = distances[node]
if current_node is None:
break
visited.add(current_node)
for neighbor, weight in graph[current_node]:
candidate = distances[current_node] + weight
if candidate < distances[neighbor]:
distances[neighbor] = candidate
return distances
graph = {
"A": [("B", 4), ("C", 1)],
"B": [("D", 1)],
"C": [("B", 2), ("D", 5)],
"D": [],
}
print(dijkstra(graph, "A"))
输出为:
{'A': 0, 'B': 3, 'C': 1, 'D': 4}
How does it really work?
考虑以下有向边:
- ;
- ;
- ;
- ;
- 。
从 出发:
- 初始时 ,其余为 。选定 后,得到 、。
- 未确定节点中 最小,选定 。松弛后, 从 改为 ,。
- 接着选定 ,因为 。通过 , 从 改为 。
- 最后选定 。
最终距离是:、、、。其中到 的最短路径是 ,到 的最短路径是 。
也可以参考 python tour 来看可视化过程。
Dijkstra’s algorithm不能处理负权边,即使图中没有负环也不行。原因不是实现细节,而是上面的贪心证明依赖边权非负。若图可能包含负边,应使用 Bellman–Ford 算法;它还能检测从源点可达的负权环。