黏菌没有大脑,3000 个 Physarum 粒子跑 12973 步自己铺出了迷宫最短路

60×40 的迷宫有 2400 格,起点在左上、终点在右下,BFS 量出来最短的一条要走 409 格 ;把 3000 个只有「左前、正前、右前三个传感器」的黏菌粒子丢进去,它们在第 12973 步 第一次把起点和终点连起来,连出来的通路长度也是 409 格 ,冗余比 1.00。同一张迷宫、同一个种子连跑两次,两次都停在 12973 步------这个数字是确定的,你可以自己复现。

公开仓库:atomgit.com/deli007/dem...,运行命令就是双击 index.html(或者 python3 -m http.server 8000),页面自带 14 条断言的自检面板,交付时全部通过。下面每个数字,都是我把交付的页面落到本机、自己重新跑一遍读出来的,不是转述对话里的结论。

边界先说清楚:这不是「黏菌比算法快」,也不是「AI 会算最短路」。页面上只有一件事:让一堆没有记忆、没有全局视野的粒子靠信息素互相通信,看看一条路能不能自己「长」出来。

一、先看结果:换个传感器角度,连通速度差出一个量级

页面右侧有一张「传感器角度对照表」:同一张迷宫、同一个种子、同样跑 20000 步,只换传感器角度,让三组参数各跑一遍。我在页面上点了「开始对照」,等它跑满 20000 步,把表格抄下来(对照组的粒子数是 2000):

传感器角度 首次连通步数 黏菌通路长度 冗余比
22.5° 7213 409 1.00
45° 13274 409 1.00
90° 未连通(20000 步内) - -

我不太信「90° 只是慢」这个说法,又把角度单独拉到 3000 粒子、最多 60000 步各跑一遍:

传感器角度(3000 粒子) 首次连通步数 黏菌通路长度
22.5° 7259 409
45° 12973 409
90° 60000 步仍未连通 -

同一个模型,把传感器的张角从 22.5° 张到 90°,首次连通从 7259 步变成 12973 步;再张到 90°,六万步都连不上。主实验(默认参数)的完整读数是这样:

指标 读数
迷宫 / 种子 60×40 / 42
粒子数 / 传感器角度 3000 / 45°
基准最短路(BFS) 409 格
首次连通步数 12973
黏菌通路长度 409 格
冗余比 1.00
同一组参数连跑两次 都是 12973 步、都是 409 格
连通后继续跑到 60000 步 通路仍是 409 格
信息素峰值 251.22

二、为什么值得看:没有大脑的东西,为什么能「算」出一条路

黏菌(Physarum polycephalum)是一种没有神经系统的单细胞生物。2010 年《Science》上有一篇很有名的工作:研究者把燕麦片摆成东京周边城市的相对位置,让黏菌自己长,最后长出来的输送网络在「总长度」和「容错性」两个指标上,和真实的东京地铁网络差不太多。它当然不知道东京在哪,它只是做了三件事:

  1. 正反馈:走过的路上留下信息素,后面的粒子更愿意跟着走;
  2. 负反馈:信息素会蒸发,绕远的路慢慢淡掉;
  3. 局部感知:每个粒子只有三个传感器,看左前、正前、右前哪边的浓度高,就往哪边偏一点。

这三条加起来,就是「短的路被反复加固、长的路自己消失」。本页里的每个粒子就 3 个数字:位置 x、y 和朝向角;它不知道起点到终点有多远,也不知道迷宫长什么样。这也是我觉得它比「又一个小游戏」值得做的地方------你能在页面上看到一条通路从无到有,而且它旁边的橙色线就是标准答案,随时可以对比。

三、准备环境:打开码道 Web

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示,浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source...,登录后就能在对话窗口里输入需求,不需要装软件。

四、任务描述:把「可验收的量」写进需求

需求我改了不止一遍,核心是把「我要看什么数字」写死。下面这段是原话(节选):

text 复制代码
做一个单文件 index.html 的「黏菌寻路实验室」(Physarum / slime mold 模型),中文界面、零外部依赖、双击就能跑、不联网。
1) 画布用格子迷宫(默认 60×40 格,可调 30×20 ~ 100×70,固定随机种子可复现),起点(食物源 A)在左上角,终点(食物源 B)在右下角。
2) 模拟黏菌:默认 3000 个粒子(可调 200 ~ 8000),每个粒子有位置和朝向;每一步用「三传感器」规则:沿朝向在左前、正前、右前三个点采样信息素浓度,朝浓度最高的一侧转「转身角」,然后前进一格,并在身后沉积信息素。
5) 实时指标面板:当前步数、首次连通步数(信息素浓度超过阈值的格子第一次让起点和终点连通的那一步)、黏菌通路长度(在「浓度 > 阈值」的格子上再跑一次 BFS 得到的起点→终点路径长度)、基准最短路长度、冗余比。
8) 页面里要有自检面板,至少 12 条断言:固定种子下同一迷宫两次生成完全一致;BFS 最短路在一个手算的小迷宫里等于手算值;参数越界会被拒绝;沉积与蒸发之后信息素不出现负数或 NaN;连通后冗余比 ≥ 1;传感器三个采样点坐标随朝向旋转的几何正确性。每条断言后面显示实测值。
9) 失败路径:粒子数输入 0、负数、非数字或超范围时,给中文提示并保留上一次的有效值,页面不能白屏。
另外做一张「传感器角度对照表」:固定同一个迷宫、同一种子、同样步数,分别用 22.5° / 45° / 90° 各跑一次,把「首次连通步数、黏菌通路长度、冗余比」实时填进表里。

两个心得:「首次连通步数」这种能被量出来的指标,一定要写进需求 ,否则回来的就是一句「已实现」;自检也要写进需求,页面上的断言和文章里的测试数才有出处。

五、码道实际做了什么

交付的是一个 39 KB 的单文件 index.html,主要部分:迷宫生成(深搜挖路)与 BFS 基准最短路、三传感器粒子推进、双缓冲的信息素场(蒸发 + 扩散)、阈值激活 + 并查集判断「起点终点是否连通」、在激活格子上再跑 BFS 量出通路长度、实时指标面板、通路长度曲线图、角度对照表、14 条断言的自检面板和参数越界保护。它还在自己的环境里反复跑浏览器验证,其中有一轮把「粒子撞墙后完全随机转向」改成了「沿墙偏转」,原因是随机转向会让粒子在起点附近的死区里反复掉头、根本推不出去------这是这轮最有价值的一次修改。

六、代码亮点:三个传感器 + 双缓冲信息素 + 阈值连通

1)一个粒子一步只读三个采样点。 下面这段是 moveParticles() 的核心,往左前、正前、右前各采一次浓度,谁高往哪偏:

javascript 复制代码
const cl=this.sample(x+Math.cos(a-sa)*sd, y+Math.sin(a-sa)*sd);
const cf=this.sample(x+Math.cos(a)*sd,      y+Math.sin(a)*sd);
const cr=this.sample(x+Math.cos(a+sa)*sd,   y+Math.sin(a+sa)*sd);
let na=a;
if(cr>cl&&cr>=cf){ na=a+ta; }
else if(cl>cr&&cl>=cf){ na=a-ta; }

三个采样点都在前方 sd 距离处(默认 2 格),sa 是传感器半角(默认 45°),ta 是转身角(默认 30°)。三个方向浓度几乎一样时,代码会加一点点随机扰动,避免整群粒子锁死在一个方向上。

2)信息素用两块 Float32Array 轮流写。 蒸发和扩散在同一次遍历里做完,N 写、P 读,遍历完再交换指针,不需要额外的临时数组:

javascript 复制代码
let v=P[i]*diag;
if(x>0&&g[i-1]===1) v+=P[i-1]*df;
if(x<W-1&&g[i+1]===1) v+=P[i+1]*df;
if(y>0&&g[i-W]===1) v+=P[i-W]*df;
if(y<H-1&&g[i+W]===1) v+=P[i+W]*df;
N[i]=v*ev;

df 是扩散系数、ev 是蒸发系数、diag=1-4*df,保证四个邻居分走的量加起来刚好等于原来的量(扩散不失真)。

3)「连通」和「通路长度」是两件事。 先按阈值把格子激活,用并查集判断起点终点是不是同一组;连通之后,再在激活格子上跑一次 BFS 量长度:

javascript 复制代码
if(grew&&this.firstConnectStep<0&&uf.connected(si,ei)){
  this.firstConnectStep=this.stepCount;
}
if(this.firstConnectStep>=0){
  this.pathLen=this.bfsActivated();
}

顺带一句:这里的「最短路」是四邻域的格子最短路,不允许走斜线;换成八邻域或者连续空间,409 这个数会变。

七、验证与踩坑

自检 14 条,14/14 通过。 页面加载时自己跑一遍,点「重新自检」可以重跑。我把几条带实测值的抄下来:

  • 固定种子可复现:种子 12345 两个迷宫各 1350 格,逐格比对完全一致;
  • BFS 在手动小迷宫上等于手算:5×5 迷宫得到 9 个顶点,手算期望 9 个;
  • 参数越界拒绝:0、-5、abc、9000 全被拒绝,8000、200、3000 接受;
  • 信息素数值健康:80 轮蒸发 + 扩散后最小值 0.0000,NaN / 负数 0 个;
  • 扩散总量守恒:扩散前总量 100.0000,扩散后 100.0000,相对误差 0;
  • 传感器三点随朝向正确旋转:0° 时正前采样点 (13.00, 10.00),转 90° 后 (10.00, 13.00);
  • 起点与终点都在空地上:起点 (1,1)、终点 (57,37);
  • 多种子多尺寸可达:4 个种子 × 4 种尺寸共 16 组迷宫全部 BFS 可达。

踩坑一:扩散系数不是「越小越好」,我改大了反而更快。 页面默认扩散系数 0.1。我照直觉把它调到 0.2,想看看会不会更早连通------结果是提前了:同样 3000 粒子、45°、种子 42,0.2 在第 12162 步连通,0.1 要等到 12973 步,通路都是 409 格。这条我没法用「扩散大 = 信息素糊成一片」之类的说法解释掉,只能说明参数之间是互相耦合的,想调优就得一组组试,别凭直觉。

踩坑二:角度这张表里藏着一条硬边界。 90° 传感器不是「慢」,是在 60000 步内根本没打通(20000 步的对照表里它也是「未连通」)。原因是三个采样点张得太开,粒子已经不知道该往哪偏了。这条边界我原样留在文章里,因为它恰好说明:能连通不是理所当然的,是 22.5° / 45° 这两档参数加上起点、终点两个食物源一起撑出来的。

还有两条我写清楚,免得读成夸大的结论:

  • 冗余比 1.00 的含义。 它的算法是「在信息素超过阈值的格子上,再跑一次 BFS」------所以它天生 ≥ 1,1.00 只能说明黏菌的浓度通道里确实存在一条和 BFS 等长的最优走廊,不代表黏菌算得比 BFS 快;
  • 首次连通步数会随粒子数变。 同样 45°、同样种子,3000 粒子是 12973 步,2000 粒子是 13274 步。数字不是常数,是和参数绑在一起的。

八、亲手点三下(第三下是失败路径)

  1. 看关键数值:保持默认(60×40、种子 42、3000 粒子、45°),等「首次连通步数」跳出来,读 12973 和通路长度 409,对比「基准最短路长度」409;
  2. 换个角度:把传感器角度改成 22.5°,点「开始对照」,等对照表填完------你会看到 22.5° 明显更快,而 90° 那一行是「未连通」;
  3. 走一遍失败路径 :把「粒子数」改成 0(或者 -5、abc、9000),页面不该白屏、也不该静默吞掉这次输入,而是给中文提示并把上一次的有效值放回去:
text 复制代码
粒子数需为 200 ~ 8000 的整数,已保留上一有效值 3000

同一个保护套在所有能填数字的地方:传感器距离、转身角、沉积量、蒸发系数、扩散系数、食物源强度、连通阈值、迷宫尺寸、随机种子,越界一律给中文提示并回退。

九、本地复现

bash 复制代码
git clone https://atomgit.com/deli007/demo_park.git
cd demo_park/codearts-physarum-slime-mold
python3 -m http.server 8000     # 或者直接双击 index.html,效果一样

打开页面后按 F12 看 Console,正常是干净的;页面里没有任何 CDN、字体、构建步骤,也不发联网请求。底部自检面板的 14 条断言实时刷新;「重新自检」可以手动重跑一次。

十、直接下载试玩

这个案例已经完整开源(源码、本文档、5 张截图),可以直接下载来跑:

十一、使用码道体会

  • 需求里的「可验收量」越多,回来能验的东西越多。 这次最值钱的是一句「首次连通步数、通路长度、冗余比都要显示出来」------交付时这些数字都摆在页面上,我读一眼就能验收,不用翻代码。
  • 让它自己开浏览器跑,比读代码有效。 「撞墙随机转向导致粒子在起点死区掉头」这种问题,静态看代码很难发现,跑起来几秒就露馅了。
  • 失败路径的文案要写进需求。 我要求「非法输入给中文提示并保留上一次有效值」,交付后的提示原文就是可验收的;不写这句,很容易拿到一个「默默还原成默认值」的实现。
  • 自检面板值得单独要求。 页面自己带 14 条断言,我改参数、改代码之后不用重新搭环境,点一下就知道有没有踩坏东西。
  • 参数要能现场调。 粒子数、角度、距离、蒸发、扩散都是活的:把粒子数从 3000 调到 200,或者把扩散从 0.1 调到 0.2,连通步数就变------这比一张静态截图有说服力。

十二、总结

一个 39 KB 的单文件网页,把「黏菌会算最短路」这句听起来很玄的话拆成了三步:三个传感器、一次沉积、一次蒸发。默认参数下 3000 个粒子在第 12973 步把起点和终点连起来,通路长度 409 格,和 BFS 给的基准最短路完全一样(冗余比 1.00);把传感器角度从 22.5° 张到 90°,首次连通从 7259 步一路恶化到 60000 步都连不上。想继续玩的话,可以试试把迷宫换成 30×20 或 100×70、把种子换一个,看看「首次连通步数」怎么变;也可以把扩散系数调大一点,我实测过它会让连通变快,理由留给你自己找。

说明:文中所有读数(三组角度的首次连通步数、通路长度、冗余比、信息素峰值、自检条目与实测值、两处参数对照、错误提示原文)都是我在本机实际运行页面取到的,不是转述。文中引用的三处代码均来自交付的 index.html 原文。

相关推荐
凌云若寒1 小时前
BarTender提示#807错误:无法在拥有其他许可证的Licensing Service上激活节点锁定的 Professional 版许可证 的解决办法
运维·服务器·前端·学习·软件需求
嘟嘟同学和妮妮同学1 小时前
用原生 JS 做了一个中国历史帝皇梳理的可视化站(7 朝 77 帝 + Leaflet 地图)
前端
黄权光1 小时前
【微信小程序】uni-app + Vue3 + Vite 的小程序项目实现「进入指定范围才能打卡」的考勤功能
前端
西柚小萌新1 小时前
【LLM&&AI应用开发 八股文】--4.2.Agent智能体(中)
前端·javascript·react.js
逐米时代1 小时前
远程运维诊断减少到场率
java·服务器·前端
Java后端的Ai之路2 小时前
03_React_JSX
前端·react.js·前端框架
颜进强2 小时前
26 · NestJS 基础篇 · 全专栏总结:五个阶段,一次收拢
前端·后端·ai编程
风寄巴山秋2 小时前
OpenBMC:Web 页面功能异常排查
运维·服务器·前端·架构
程序员Sunday3 小时前
Promise.all、allSettled、race、any 怎么选,失败后其他请求会怎样
开发语言·前端·javascript