泛洪:把「找一条路」换成「给每一格记一个数」
全课题骨架:寻路不是"找一条路",而是"给地图上每个位置记一个数,然后从任意一点顺着这个数往下走" ——
而记数这件事只有一条规则:从已经确定的那一圈往外推一格,给新格记下它自己的数。
四段路:先把规则写成"取最小的那一格、推它的四个邻居"(24.1)→ 给每一格一个价,规则一个字不改(24.2)→
问一句"什么时候可以停"(24.3)→ 把这同一张数表换一个看法:路就是"从终点往回走下坡"(24.4)。
★ 量出来才敢写的一件事:泛洪算出来的那张数表,与"按层一圈圈扩"这条完全独立的路线逐格相同 ——
965 个可走格,一位不差(实测 965 / 965)。
以及:把泥地标成"贵 4 倍"之后,最短路自己改道了 —— 单位权下 31 步穿 7 格泥,带权下 47 步一格泥都不穿;
老那条路线按带权算要 52,比最优的 47 贵 5。
保真度:示意 · 等价重现 —— 泛洪、参考解、连通块、梯度下坡都是纯图算法
(页面现场跑的十条与离线定稿 q241.mjs 同口径),画面是本页自己的二维软件光栅
(逐格填色 + 网格线 + 路径折线),不是任何引擎或导航库的内部实现。
★ 本页的优先队列用"线性扫一遍取最小"实现(1000 个格,最直白也最可复算);
不做 JPS / NavMesh / 流场(那是后面四课),也不做对角走位(只有四邻)。
本页的原理与四张数值表照常可读(画面只是把同一张数表涂出来)。
(脚本启动中…)
定点观察:(脚本启动中…)
本帧的读数
(脚本启动中…)
24.1 段 · 从起点往外推一圈
本段只增加一样东西:把"路"换成一张数表(读者只动「单步 / 跑到底」,画面上一格一格地长出数来)。
保真度:示意 · 等价重现。读者默认以为寻路是"找一条线";这里换掉这个想法:
先给每一格记一个数 d(从起点走到这儿花了多少),路只是这张表的一个副产品。
记数只有两条动作:① 从"待定"里取出 d 最小的那一格,把它定为"已定";② 把它的四个邻居按
cand = d + w 记上数——第二条就是全课题的那条唯一规则。
★ 一手源把这两条写成一句话(Dijkstra 1959 原话):"the minimal paths from P to the other nodes are constructed
in order of increasing length until Q is reached."(最短路径是按长度递增的顺序被构造出来的,直到 Q 被取到。)
★ 本段的账是两条独立路线:一条是"取最小的往外推",另一条是"按层一圈圈扩"(BFS 层数)——
两者不共用任何中间量,965 个可走格逐格相同(实测 965 / 965,一个不同都没有)。
st.step(逐字,含注释),右列是实测。
注意中间那两行 const cand = d[n] + w; 与 d[m] = cand; ——
全课题后面四课换的都只是这两行的"数"是什么,推邻居这个动作一个字都不改。| 记的账 | 应然 | 实测 | 结论 | 怎么算 | 类别 |
|---|
定点观察:① 反复按「单步」(走完再按一次会从头重来); ② 看画面里浅色那一圈(待定)怎么长、深色那一块(已定)怎么跟着吃过去, 以及读数里"已定 / 待定"两个数怎么变; ③ 结论:被定为"已定"的顺序是按 d 从小到大排的 —— 这就是"路"能被读出来的原因, 而它其实是这张数表的一个副产品。
24.2 段 · 让每一格有自己的价
本段只增加一样东西:边权(读者只动「地形权重」那对按钮,别的一切与 24.1 相同)。
保真度:示意 · 等价重现。上一段里每一格都"值一步",可真实地图不是这样:泥地难走、草地好走。
本段的动作只有一处:把 w 从"恒等于 1"换成"看这一格是什么地形"——
规则本身(cand = d[n] + w)一个字符都没改。
★ 结果很直观,但它值得单独说一遍:最短路自己改道了。单位权下 31 步、要穿 7 格泥;
把泥地标成 4 倍之后,最短路变成 47 步、一格泥都不穿。
把老那条路线拿来按带权算一遍是 52,比最优的 47 贵 5 ——
也就是说泛洪不是"沿旧路径打补丁",而是重新算了一遍,并且找到了更便宜的走法。
★ 一句话点题:这一步之后,"最短"这两个字的含义从步数最少变成了代价最小;
本页后面所有提到"最短"的地方,都是指后者。
| 记的账 | 应然 | 实测 | 结论 | 怎么算 | 类别 |
|---|
定点观察:① 在「地形权重:关 / 开」之间来回切; ② 看画面里那条高亮的路径 —— 数一数它穿过的深色泥地格; ③ 结论:权重一开,路径自己绕开了泥地,代价从 52 掉到 47, 而这一步只改了"价从哪来"这一个地方。
24.3 段 · 什么时候可以停
本段只增加一样东西:早退(读者只动「停止条件」那对按钮)。
保真度:示意 · 等价重现。前两段一直把整张地图算完 —— 可读者要的只是到目标那一格。
本段加的那一行是:从"待定"里取出来的那一格正好是目标格时,就停。
★ 凭什么停?因为取的时候是按 d 从小到大取的:目标格的 d 已经是最小的那个数,
后面还没被取的格 d 只会更大,它们不可能再给出更小的目标值。
换句话说:"取到目标 ⇒ 它的数已经定了",这不是近似,是规则本身的性质。
★ 省下来的账是实打实的:跑满全图要定 965 格,到目标就停只定 774 格,
省下 191 格(19.8%);而两档的 d(目标) 都是 47,一个字都没差。
★ 一手源原话(HN R 1968)把顺序也写清了:"Select the open node n whose value of f is smallest."——
取最小的那一个,这就是"能停"的全部依据。
| 记的账 | 应然 | 实测 | 结论 | 怎么算 | 类别 |
|---|
定点观察:① 在「跑满全图 / 到目标就停」之间来回切,并按「单步」看过程;
② 看读数里"已定"那个数停在哪儿,以及 d(目标) 那一项;
③ 结论:早退把要定的格从 965 降到 774,而目标那一格上的数一模一样 ——
这不是"近似解",是"同一个答案、更早拿到"。
24.4 段 · 把这张数表当地形看(第二视角)
本段只增加一样东西:看法(读者只动「看法」那对按钮,一个数都不重算)。
保真度:示意 · 等价重现。前三段把 d 当成"记在格子上的一笔账";
现在换一个正交的看法:把 d 当成高度——起点是谷底(0),越远越高,墙是悬崖。
在这个地形上,"路"就是一件很自然的事:从终点出发,每一步都往更小的 d 走,一路下坡就回到起点。
★ 这两条读法是同一张表的两种用法:24.1–24.3 是"从起点往外记",
本段是"从终点往里读"(par 往回回溯是另一条等价读法,两条实测步数相等:47 = 47)。
★ 实测的下降序列(每 8 格取一个):47 → 39 → 31 → 23 → 15 → 7 → 0,全程严格递减、最后恰好落在 0。
为什么一定下得去?因为每一格的 d 都是"某个邻居的 d 加上一步"得来的 ——
所以那一格必然存在一个更小的邻居,只要一直挑最小的那个,就一定能走到起点,不会卡在半路。
| 记的账 | 应然 | 实测 | 结论 | 怎么算 | 类别 |
|---|
定点观察:① 在「两圈着色 / 数表热力 + 下坡」之间切换; ② 看画面 —— 热力越亮说明 d 越小(离起点越近),那条亮线就是"一路下坡"; ③ 结论:同一条路径,24.1 是"从起点往外记出来的",24.4 是"从终点往里读出来的" —— 两种读法给出同一张表、同一条路(47 = 47)。
本课的收尾
这一课只做了一件事:把"找一条路"换成"给每一格记一个数"。
往下的缺口很直白:泛洪对整张地图一视同仁,它朝四面八方推,而终点只在一个方向 ——
所以它把很多根本不需要的格也定了数(本页 965 格里,真正与最优路径有关的只是其中很小一部分)。
下一课(24.2 → 第 2 课 · 猜一个数)加的那一样东西就是:数里再放一项"从这一格到终点还要花多少"的猜测,
取的时候按"已经花的 + 猜的还要花的"来取 —— 于是搜索会朝终点那边偏,而答案能不能保持不变,
就取决于那个猜测守不守一条规矩(不高估)。
★ 本页留了一个可以自己动手的口子:在画面上点格子可以把墙加上或去掉。
点满一列(把 x = 20 那道竖墙的三个口子封上),再按一次「跑到底」——
你会发现 d(目标) 变成 ∞(目标压根定不上数),
而"已定格数"恰好等于与起点连通的那一片(实测封口后 500 = 500,见判据⑥)。
自检面板
等待脚本…