LeetCode 909. 蛇梯棋:解题思路与代码详解

0 阅读8分钟

在LeetCode的中等难度题目中,蛇梯棋(Snakes and Ladders)是一道典型的“最短路径”问题,核心考察广度优先搜索(BFS)的应用。这道题的难点不在于算法本身,而在于理解棋盘的编号规则、蛇和梯子的跳转逻辑,以及如何将棋盘编号与二维数组的坐标进行正确映射。今天我们就来一步步拆解这道题,从题目分析到代码实现,搞懂每一个细节。

一、题目核心解读

先明确题目给出的核心规则,避免理解偏差导致解题出错:

  • 棋盘是 n x n 的矩阵,方格编号从 1 到 n²,编号规则特殊:从左下角(board[n-1][0])开始,转行交替方向(即奇数行从左到右,偶数行从右到左,这里的行数是从下往上数的)。

  • 初始位置是方格 1,目标是到达方格 n²,游戏结束。

  • 每回合掷骰子,可前进 1~6 步(对应 curr+1 到 min(curr+6, n²))。

  • 跳转规则:如果前进到的方格有蛇或梯子(board[r][c] != -1),则直接跳转到 board[r][c] 对应的方格;注意:每次掷骰子后,最多只能跳转一次(即使跳转后的方格还有蛇/梯子,也不能继续跳)。

  • 求到达目标方格的最少掷骰子次数,无法到达则返回 -1。

举个简单例子帮助理解:棋盘 [[-1,4],[-1,3]](n=2),初始在 1。掷骰子到 2,此时 2 对应方格有梯子(值为 3),则跳转到 3,但不能再从 3 跳转到 4(即使 3 也有梯子)。

二、解题思路:为什么用 BFS?

这道题的核心诉求是“最少掷骰子次数”,本质是求“从起点 1 到终点 n² 的最短路径长度”——每一次掷骰子就是一步,路径长度就是掷骰子次数。

对于“最短路径”问题,BFS 是最优选择:BFS 按层次遍历,第一次到达终点时,所经过的步数就是最少步数。因为 BFS 不会像 DFS 那样深入一条路径到底,而是逐层扩散,确保首次抵达终点时的步数是最小的。

解题的关键步骤拆解:

  1. 将棋盘编号(1~n²)与二维数组的坐标(r,c)进行映射(这是最容易出错的一步)。

  2. 用 BFS 队列存储当前位置和已掷骰子次数,队列元素格式为 [当前方格编号, 步数]。

  3. 用一个访问数组(vis)记录已访问过的方格,避免重复访问(防止陷入循环,比如蛇的往返跳转)。

  4. 遍历每一次掷骰子的可能(1~6步),计算下一个方格的编号,处理跳转逻辑,判断是否到达终点,若未访问则加入队列。

三、关键难点:编号与坐标的映射(getRC 函数)

题目中编号的转行交替规则是核心难点,我们需要写一个辅助函数 getRC,输入方格编号 id 和棋盘大小 n,输出该编号对应的二维数组坐标(r,c)。

映射逻辑推导(结合示例理解):

  1. 先计算当前编号所在的“层”(从下往上数的行数):r_raw = Math.floor((id - 1) / n)。比如 id=1,n=2,(1-1)/2=0,即第 0 层(最下层);id=3,(3-1)/2=1,即第 1 层(上层)。

  2. 计算在当前层内的列索引(从左到右):c_raw = (id - 1) % n。比如 id=2,(2-1)%2=1,即第 1 列(从左数)。

  3. 处理转行交替方向:如果当前层 r_raw 是奇数(上层),则列索引反转(从右到左),即 c = n - 1 - c_raw;如果是偶数(下层),则列索引不变。

  4. 将“从下往上的层”转换为二维数组的行索引(从上往下数):原数组的行索引 r = n - 1 - r_raw。因为原数组的第 0 行是最上层,而我们的 r_raw 是从下往上数的。

举个例子验证:n=2,id=2

  • r_raw = (2-1)/2 = 0(偶数层,从左到右)

  • c_raw = (2-1)%2 = 1

  • r = 2-1 - 0 = 1(原数组的第 1 行,即最下层)

  • c = 1(因为 r_raw 是偶数,不反转)

  • 对应棋盘 board[1][1],与题目示例 [[-1,4],[-1,3]] 中 id=2 的位置一致(值为 3),正确。

四、完整代码与逐行解析

下面是完整的 TypeScript 代码,结合注释逐行解析,重点关注 BFS 逻辑和映射函数:

function snakesAndLadders(board: number[][]): number {
  const steps = 6; // 骰子最大点数
  const n = board.length; // 棋盘大小 n x n
  const target = n * n; // 目标方格编号
  const vis = new Array(target + 1).fill(0); // 访问标记数组,索引对应方格编号
  vis[1] = true; // 初始位置 1 已访问
  const queue: number[][] = [[1, 0]]; // BFS队列:[当前方格编号, 已掷骰子次数]

  while (queue.length) { // 队列不为空,继续遍历
    const curr = queue.shift(); // 取出队首元素(BFS 先进先出)
    if (!curr) continue; // 防止空值报错
    const [currId, currStep] = curr; // 解构当前位置和步数

    // 遍历掷骰子的6种可能(1~6步)
    for (let i = 1; i <= steps; i++) {
      let nextId = currId + i; // 下一个方格编号(未处理蛇/梯子)
      if (nextId > target) break; // 超出目标,直接跳过(后续i更大,无需继续)

      // 调用辅助函数,获取nextId对应的棋盘坐标
      const [row, col] = getRC(nextId, n);
      // 获取该坐标对应的蛇/梯子目的地(-1表示无)
      const boardVal = board[row][col];

      // 处理蛇/梯子跳转:如果有蛇/梯子,更新nextId为跳转后的编号
      if (boardVal > 0) {
        nextId = boardVal;
      }

      // 到达目标,返回当前步数+1(本次掷骰子算一步)
      if (nextId === target) {
        return currStep + 1;
      }

      // 如果未访问过,标记为已访问并加入队列
      if (!vis[nextId]) {
        vis[nextId] = true;
        queue.push([nextId, currStep + 1]);
      }
    }
  }

  // 队列遍历完仍未到达目标,返回-1
  return -1;
};

// 辅助函数:将方格编号id转换为棋盘的(row, col)坐标
const getRC = (id: number, n: number): number[] => {
  let rRaw = Math.floor((id - 1) / n); // 从下往上数的层数
  let cRaw = (id - 1) % n; // 层内从左到右的列索引

  // 奇数层(从下往上数),列索引反转(从右到左)
  if (rRaw % 2 === 1) {
    cRaw = n - 1 - cRaw;
  }

  // 转换为原数组的行索引(从上往下数)
  const row = n - 1 - rRaw;
  return [row, cRaw];
};

五、代码关键细节说明

  • 访问数组 vis:为什么用数组?因为方格编号是 1~n²,连续且有序,用数组索引直接对应编号,查询和标记效率都是 O(1)。

  • 队列操作:用 shift() 取出队首元素(BFS 特性),但注意在 JavaScript/TypeScript 中,数组 shift() 是 O(n) 时间复杂度,若 n 很大(比如 200,n²=40000),效率会受影响。优化方案:用双端队列(Deque),比如用链表实现,或使用数组的 push() 和 splice(0,1) 替代(本质还是 O(n),但题目约束 n≤200,完全够用)。

  • 跳转逻辑:只有当 board[row][col] > 0 时才跳转(题目中蛇和梯子的目的地都是有效编号,不会为 -1),且跳转后直接更新 nextId,不再继续跳转(符合题目“最多跳转一次”的规则)。

  • 终止条件:当 nextId === target 时,直接返回 currStep + 1,因为 BFS 首次到达终点,步数最少,无需继续遍历。

掌握了代码的核心实现和细节后,我们再梳理一下做题过程中容易踩坑的地方,帮助大家避开错误、高效AC。

六、常见错误与避坑点

  1. 坐标映射错误:忘记反转奇数层的列索引,或混淆“从下往上的层”与“原数组的行索引”,导致访问到错误的棋盘方格,进而计算错误。

  2. 重复访问:未使用 vis 数组,导致同一个方格被多次加入队列,陷入循环(比如蛇从 5 跳到 3,又从 3 跳回 5)。

  3. 跳转逻辑错误:多次跳转(比如跳到一个有梯子的方格后,又继续跳该梯子的目的地),违反题目规则。

  4. 终止条件判断错误:在处理完跳转后才判断是否到达目标,而不是在跳转前(虽然不影响结果,但会多做一次判断,效率略低)。

梳理完避坑点,我们对这道题做一个完整总结,帮助大家巩固核心知识点。

七、总结

蛇梯棋这道题,核心是用 BFS 求解最短路径,难点在于理解棋盘编号规则并实现正确的坐标映射。解题流程可以总结为:

  1. 明确问题本质:最短路径 → BFS 求解;

  2. 解决核心难点:实现编号与坐标的映射(getRC 函数);

  3. 处理边界条件:蛇/梯子的跳转规则、访问标记、终止条件;

  4. 代码优化:根据语言特性调整队列操作,提升效率。

只要掌握了 BFS 的核心思想,再理清坐标映射的逻辑,这道题就能轻松 AC。如果在做题过程中遇到坐标映射错误,可以多举几个小例子手动计算,加深理解。