> 本文从零开始讲解 Dijkstra 算法的核心思想------贪心策略与最短路径树,并通过一个完整的例子手把手带你推演每一步的距离更新,彻底理解"为什么这样操作就能找到所有节点的最短路径"。
一、算法定位与问题定义
Dijkstra 算法 由荷兰计算机科学家 Edsger W. Dijkstra 于1956年提出,用于解决单源最短路径 问题,是图论中最经典的算法之一。它的本质是贪心算法,逻辑上无法脱离贪心,不存在所谓"非贪心版 Dijkstra"。
问题描述
- 给定一张图 G = (V , E ),共有 v 个节点,e 条边(可以是无向或有向图)
- 每条边带有一个非负的权值(称为距离、权重或开销)
- 指定一个源点 s ,求从 s 到图中所有其他可达节点的最短路径及其长度
⚠️ 注意 :Dijkstra 算法要求所有边的权重非负。如果存在负权边,贪心策略将失效,需要使用 Bellman-Ford 等算法。
二、核心思想:贪心地构建最短路径树
Dijkstra 算法的精髓在于:按照与源点距离从小到大的顺序,逐个确定每个节点的最短路径,并在确定后不再更改 。整个过程可以看作是在以 s 为根,逐步"生长"出一棵最短路径树。
为什么贪心是正确的?
想象我们从 s 出发探索整张图:
- 第一个最近节点
离 s 最近的点,必然在直接与 s 相连的节点中。
任何不直接相连的点,都必须先经过某个直接邻居才能到达,其距离不可能小于最短的那条直连边。
因此,直接邻居中边权最小的那个节点,就是全图中距离 s 最近的点,记作 v 1,它的最短距离在此刻就已经确定。 - 第二个次近节点
次近的点可能是另一个直接邻居,也可能是通过 v 1 才能到达的点。
但它不可能 经过其他尚未确定的点------那些点本身比 v 1 更远,再绕路只会让距离更长。
所以,我们只需在「直接邻居」和「v 1 的邻居」这些尚未确定的节点中,选一个总距离最小的,它就是次近点 v 2。 - 依次类推
每当我们确定一个节点,就把它加入"已确定集合",并用它去尝试更新它的邻居的距离(即松弛操作 )。
之后,从未确定节点中再次选出距离最小的那个,它的最短路径就可以确定下来。
如此反复,就像是从根 s 开始,一圈一圈向外扩展,每次选出最近的一个新节点挂到树上。这棵树最终包含 s 和所有可达节点,树上从根到任意节点的路径,就是该节点的最短路径。
最短路径树
- 以源点 s 为根
- 每个节点通过唯一的前驱边连接到树中
- 树的节点集合 = 所有从 s 可达的节点
- 树上 s ⇝ v 的路径权重和 = s 到 v 的最短距离
三、算法步骤
用伪代码描述如下:
1. 初始化
- 距离数组 dist[s] = 0,其他节点 dist[v] = ∞
- 已确定最短路径的节点集合 S = ∅
- 前驱数组 prev[v] = null
2. 循环,直到所有节点都被确定或最小距离为 ∞(不可达)
- 从未确定节点中,选择 dist 值最小的节点 u
- 将 u 加入 S(此时 dist[u] 就是最终的最短距离)
- 对 u 的每个邻居 v:
- 若 dist[u] + weight(u, v) < dist[v]:
- 更新 dist[v] = dist[u] + weight(u, v)
- 设置 prev[v] = u
3. 结束
循环直到:所有可达节点都已被确定,或者在某次迭代中选出的最小距离节点,其 dist 为 ∞ (这表明剩余节点全部不可达)。
四、实例演算

我们用一张具体的图来完整走一遍算法,图中的节点和边权如下:
| 起点 | 终点 | 边权(距离) |
|---|---|---|
| 0 | 1 | 2 |
| 0 | 2 | 6 |
| 0 | 3 | 7 |
| 1 | 3 | 3 |
| 1 | 4 | 6 |
| 2 | 4 | 1 |
| 3 | 4 | 5 |
源点:0,目标:求出 0 到 1、2、3、4 的最短路径。
4.1 初始状态
- dist[0] = 0
- dist[1] = dist[2] = dist[3] = dist[4] = ∞
- 已确定集合 S = {0},未确定集合 U = {1,2,3,4}
4.2 逐步推演
第1步:从 0 开始松弛邻居
- 更新邻居距离:
- 0→1:0+2 = 2 < ∞ → dist[1] = 2
- 0→2:0+6 = 6 < ∞ → dist[2] = 6
- 0→3:0+7 = 7 < ∞ → dist[3] = 7
- dist[4] 仍为 ∞
- 当前 dist = [0, 2, 6, 7, ∞]
- 选出最小未确定节点:1(距离=2)
- 加入 S:S = {0,1},U = {2,3,4}
第2步:用节点 1 松弛邻居
- 1 的邻居:3、4
- 1→3:dist[1] + 3 = 2+3 = 5 < 7 → 更新 dist[3] = 5
- 1→4:dist[1] + 6 = 2+6 = 8 < ∞ → 更新 dist[4] = 8
- 当前 dist = [0, 2, 6, 5, 8]
- 选出最小未确定节点:3(距离=5)
- 加入 S:S = {0,1,3},U = {2,4}
第3步:用节点 3 松弛邻居
- 3 的邻居:4
- 3→4:dist[3] + 5 = 5+5 = 10 > 8 → 不更新
- 当前 dist = [0, 2, 6, 5, 8]
- 选出最小未确定节点:2(距离=6)
- 加入 S:S = {0,1,3,2},U = {4}
第4步:用节点 2 松弛邻居
- 2 的邻居:4
- 2→4:dist[2] + 1 = 6+1 = 7 < 8 → 更新 dist[4] = 7
- 当前 dist = [0, 2, 6, 5, 7]
- 选出最小未确定节点:4(距离=7)
- 加入 S,U 变空,算法结束。
五、几个关键点的解释
- 松弛操作
公式 dist[v] > dist[u] + weight(u,v) 的意义:检查"经过 u 到 v"是否比已知的到 v 的路径更短,如果是,就更新。
例如本例中:- dist[3] = 7 > dist[1] + edge[1][3] = 5 → 更新为5
- dist[4] = 8 > dist[2] + edge[2][4] = 7 → 更新为7
- 贪心选择的无后效性
每次选择的节点距离一定是最短的,因为后续只可能通过已确定节点到达其他节点,而边权非负,所以距离不会再变小。 - 负权边为什么不行
如果图中存在负权边,可能会在后续发现一条绕远的路径反而总距离更短,从而推翻之前"已确定"的最短距离,贪心策略不再成立。
六、总结
- Dijkstra 算法的核心是贪心策略,每次从未确定节点中取出距离最小的节点,确定其最短路径,并用它松弛邻居。
- 整个过程等价于在图中构建一棵以源点为根的最短路径树。
- 算法实现简单,时间复杂度 O (V 2),使用优先队列可优化至 O ((V +E ) log V)。
- 限制:所有边的权重必须非负。
掌握 Dijkstra 算法,不仅能够解决单源最短路径问题,更能够深入理解"贪心正确性"的经典范例。希望本文的逐步演算能帮你彻底吃透它!