把一座城市,
算成一条最短路

一堂可以动手的 Dijkstra 算法课:先跟着导航的故事看懂它,再亲手把每一步选出来。

A 0 B 2 C 1 E 2 D 3 F 3
A 到各路口的当前最短距离,权重变化后立即更新
路口当前最短距离
A0
B2
C1
D3
E2
F3

橙色描边是 A 到 D 的当前最短路;节点旁小数字 = 当前最短距离。

不必点开始。页面加载完,这里已经是一张六个路口的两层小图:A 出发,先散到中层 B、C、E,再汇聚到 D 和 F。拨动任意一条边上的权重,会看到松弛像波浪一样从 A 逐轮传开:被更新的边逐条点亮,路口旁的小数字跟着变,最后 A 到 D 的最短路整体加粗一拍才算落定。留意那条中期看是绕远的路——只要汇聚边的权重不同,它随时可能翻盘成最终答案。

把「每次都选最近的」当成策略,在这里不是短视,而是唯一正确的做法。前提只有一条:不许有倒贴钱的路。

这句话对不对,不用信我们。往下三屏,你自己判定。

叙事:城市导航的故事

导航凭什么每次都敢选「当前最近」的路口

打开导航,输入出发地和目的地。它不会把所有走法都试一遍,而是在每一个路口只问同一句话:从我现在的位置看,哪个路口最近,就走哪个。听起来像赌运气,其实这是被数学证明过的正确策略。

证明的思路不复杂。只要每条路都不倒扣时间,先绕去更远的路口再折回来,永远不会比直接去最近的那一个更省。导航敢一次定一句话,底气全在这里:局部最省的路,拼起来就是全局最省的路。

数学家把这叫贪心选择性质。Dijkstra 的全部智慧,就是把这个朴素的策略坚持到底,每轮只做一件事,一遍都不回头。

读者此刻该做的事:对照亮起的边,看距离表哪个格子被压低了。(P2 接引擎)

A B C
橙色描边:本轮被松弛的那条边(P2 由 relax 事件驱动)。

导航说「最短走法已定」的那一刻,发生了什么

你一定听过导航说这句话。它不是安慰,而是一次数学上的承诺:从现在起,这个路口的最短距离被永久锁定,后面无论发现什么新路,都不再更改。这个承诺敢下,靠的是图中没有倒贴钱的路。

算法内部始终隔着一条边界。一边是已确定区,里面的路口 dist 再也不会变;另一边是候选区,它们的 dist 只是临时报价,随时可能被一条刚松弛的边压得更低。

每一轮,算法都从候选区里挑报价最低的那个,把它移进已确定区,再顺着它的出边更新候选区的报价。挑一次,锁一次,直到终点也进锁,答案才算落地。

读者此刻该做的事:看清这个节点是从哪个路口被拉进已确定区的。(P2 接引擎)

已确定区 候选区 A B C
橙色圆圈:候选区里报价最低的那个(P2 由 select 事件驱动)。

当一条路开始倒贴钱,贪心会陷入循环

现在放一个捣乱分子进图里:一条权重为负的边。走它不但不花时间,反而倒赚,像一段倒贴钱的顺风车。对 Dijkstra 来说,这是釜底抽薪的攻击。

麻烦在于,贪心的承诺只对不倒贴的路成立。一条负权边可以让你先绕远,再靠倒赚把总价拉得更低,于是那个「先到先锁」的路口,锁住的可能是错误答案,而且算法自己永远不会发现。

更糟的情形是,倒赚的路首尾相连,每绕一圈都能更便宜,最短路在数学上根本不存在,算法会被迫永远绕下去。所以 Dijkstra 立了一条铁规矩:图中不许有负权边。这不是它的疏忽,而是它的边界声明,也是这门课要你亲眼看到的一次翻车。

读者此刻该做的事:比较这两个数,回答「如果算法回头看一眼,答案会便宜多少」。(P2 接引擎)

A B C D −5
紫色虚线:权重为负 5 的边(P2 由 reject 事件驱动)。

翻车证据(静态示例,数据取自负权反例图)

wouldBeDist(回头看一眼) −3
dist(已锁定) 1

两者之差就是翻车的证据:C 本可以只花 −3,但它在更早一轮已被锁定为 1。

动手工具区

故事讲完了,接下来把方向盘交给你。

在预设的图上单步执行 Dijkstra,每一步都看得到距离表怎么变,负权反例也在这里翻车。

引擎将在 P2 接入

这一块面板会变成单步执行器:每按一次步进,图上的节点染色、被松弛的边亮起、距离表跟着跳动,select、relax、settle、reject 四种事件各演各的戏。

此刻先看首页的迷你演示,它已经能让你拨权重、看距离变。

选定,松弛,定格。

答案不是终点给的,是每一步正确的贪心攒出来的。