A* 迷宫寻路(Pathfinding)

由 Rust + WebAssembly 生成迷宫并逐步搜索最短路径

点击画布:

起点 终点 待扩展 已扩展 最短路径

点击「开始寻路」

切换算法后再次寻路,比较两者扩展的格子数  |  在迷宫里拆掉几堵墙,会出现多条路线

📖 原理说明

先用「递归回溯」算法生成一座完美迷宫(任意两点之间有且只有一条通路),再用 A* 或 Dijkstra 算法一步步搜索从起点到终点的最短路径。蓝色是待扩展的边界,棕色是已经扩展过的格子,橙色是最终找到的路径。

🧮算法原理

f(n) = g(n) + h(n)g:已走步数 · h:到终点的曼哈顿距离
迷宫生成(递归回溯)
迷宫以奇数坐标的格子为「房间」,房间之间隔着一格墙。从起点出发,随机挑一个没去过的相邻房间,打通中间的墙走过去;走进死胡同就沿栈回退,直到所有房间都被访问。这相当于在网格上随机生成一棵生成树,所以没有环路,每个房间都可达。
Dijkstra
每次从待扩展集合中取出「离起点最近」的格子扩展,像水波一样向四周均匀扩散。在所有边权相等的网格上,它能保证找到最短路径,但会扩展大量与终点方向无关的格子。
A*
在 Dijkstra 的基础上加入启发函数 h(n):对到终点剩余距离的估计。每次扩展 f = g + h 最小的格子,让搜索优先朝终点方向推进。只要 h 从不高估真实距离(曼哈顿距离在只能上下左右移动的网格上满足这一点),A* 同样保证找到最短路径。
优先队列与平局处理
待扩展集合用二叉堆(BinaryHeap)实现,每次取最小值的复杂度是 O(log n)。f 相同时优先扩展 h 更小(离终点更近)的格子,能进一步减少扩展数量。已经扩展过的旧堆条目直接跳过(惰性删除)。

🔄Rust 与 JavaScript 的分工

  1. Rustmaze_generate(61, 41, 种子):Rust 生成迷宫并保存在全局状态中。
  2. Rust点击「开始寻路」后调用 search_start(起点, 终点, 是否用 A*),Rust 初始化 g 值数组、父节点数组和优先队列。
  3. Rust每一帧 JS 调用 search_step(N),Rust 扩展最多 N 个格子(速度滑块控制 N = 1 ~ 512),这样就能看到搜索的动画过程。
  4. JSJS 调用 maze_cells() 取回每个格子的状态(每格 1 字节),写进一张 61×41 的小 ImageData,再关闭平滑、放大绘制到画布上。

点击画布修改墙壁或移动起点、终点时,JS 会调用 maze_toggle_wall() 或 search_clear() 清除上一次的搜索结果。

⚡性能要点

  • 同一座迷宫上,可以切换 A* 和 Dijkstra 各跑一次,比较底部显示的「已扩展格子数」。在完美迷宫里通路唯一,两者差距不算大;拆掉一些墙、让地图变得开阔后,A* 的优势会明显得多。
  • 整个 2,501 格的迷宫,搜索本身通常只需要不到 1 毫秒;动画的速度是有意放慢的,方便观察扩展顺序。
  • 每帧只传一个 2.5 KB 的字节数组,并用一次 drawImage 放大绘制,没有逐格调用 fillRect。

源码crates/algorithms/pathfind/src/lib.rswww/pathfind/index.js