图算法(Dijkstra / A*)实现最短路径与游戏寻路

本节目标

网格寻路:把地图格子当作图节点,四连通(上下左右)为边,移动代价为 1。Dijkstra 在此退化为 BFS(边权相等)。A* 在此基础上加"启发式"——估算到终点的剩余代价,优先探索"看起来更近"的方向。

// 运行环境:Node.js 14+
// 保存为 dsa-l25.js,执行:node dsa-l25.js

// 0=可走 1=障碍;返回从 (0,0) 到 (m-1,n-1) 的最短步数(Dijkstra/BFS)
function shortestPath(grid) {
  const m = grid.length, n = grid[0].length
  const dist = Array.from({ length: m }, () => new Array(n).fill(Infinity))
  dist[0][0] = 0
  const q = [[0, 0]]                       // 简单队列即可(边权=1)
  const dirs = [[1,0],[-1,0],[0,1],[0,-1]]
  while (q.length) {
    const [x, y] = q.shift()
    for (const [dx, dy] of dirs) {
      const nx = x + dx, ny = y + dy
      if (nx<0||ny<0||nx>=m||ny>=n||grid[nx][ny]===1) continue
      if (dist[x][y] + 1 < dist[nx][ny]) { dist[nx][ny] = dist[x][y] + 1; q.push([nx, ny]) }
    }
  }
  return dist[m-1][n-1]
}

// A*:加曼哈顿距离启发式 h,用"f=g+h"最小优先(演示用数组代替堆)
function astar(grid) {
  const m = grid.length, n = grid[0].length
  const h = (x, y) => (m - 1 - x) + (n - 1 - y)   // 曼哈顿距离到终点
  const g = Array.from({ length: m }, () => new Array(n).fill(Infinity))
  g[0][0] = 0
  const open = [{ x: 0, y: 0, f: h(0, 0) }]
  const dirs = [[1,0],[-1,0],[0,1],[0,-1]]
  while (open.length) {
    let bi = 0; for (let i = 1; i < open.length; i++) if (open[i].f < open[bi].f) bi = i
    const { x, y } = open.splice(bi, 1)[0]          // 取 f 最小
    if (x === m-1 && y === n-1) return g[x][y]
    for (const [dx, dy] of dirs) {
      const nx = x+dx, ny = y+dy
      if (nx<0||ny<0||nx>=m||ny>=n||grid[nx][ny]===1) continue
      const ng = g[x][y] + 1
      if (ng < g[nx][ny]) { g[nx][ny] = ng; open.push({ x: nx, y: ny, f: ng + h(nx, ny) }) }
    }
  }
  return Infinity
}

const map = [
  [0,0,0,0],
  [1,1,0,1],
  [0,0,0,0],
  [0,1,1,0],
]
console.log('Dijkstra/BFS 步数:', shortestPath(map)) // 8
console.log('A* 步数:', astar(map))                  // 8

A vs Dijkstra:Dijkstra 盲目向四周扩散;A 用启发式 h 引导方向,扩展节点更少、更快到达终点(只要 h 不高估真实代价)。

名词解释

课后练习

  1. 为什么网格图(边权=1)Dijkstra 等价于 BFS?
    • 答案:边权都相等时,最先出队的必是步数最少的节点,与 BFS"逐层扩展"完全一致,故可简单用队列代替优先队列。
  2. A* 的启发式 h 为什么不能高估?
    • 答案:若 h 高估了剩余代价,A* 可能提前选"看起来近但其实绕远"的路径并锁定,从而得不到真正最短解;保持"不高估"(可采纳/ admissible)才能保证最优。

总结

图算法在这一节落地成了你能"看得见"的东西——游戏里小人对障碍绕行、地图 App 给你规划路线,内核都是最短路径。Dijkstra 在网格上会退化成 BFS,这本身是个很好的认知桥:当所有边权相等,"最近优先"就足够。而 A* 则是工程智慧的结晶:它在 Dijkstra 的"盲目扩散"上叠加了"启发式引导",像人找路时会朝目的地方向走而不是乱转,从而大幅减少探索的节点。关键约束是启发式不能高估——这一点极具哲理,它提醒我们"带着先验方向走"能加速,但"自以为是的方向"会让你错过最优。从 Dijkstra 到 A*,你学到的不只是一个算法,而是"如何在搜索中聪明地剪枝"这一贯穿 AI、运筹、游戏的核心思想。