在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~n²)与二维数组的坐标(r,c)进行映射(这是最容易出错的一步)。
-
用 BFS 队列存储当前位置和已掷骰子次数,队列元素格式为 [当前方格编号, 步数]。
-
用一个访问数组(vis)记录已访问过的方格,避免重复访问(防止陷入循环,比如蛇的往返跳转)。
-
遍历每一次掷骰子的可能(1~6步),计算下一个方格的编号,处理跳转逻辑,判断是否到达终点,若未访问则加入队列。
三、关键难点:编号与坐标的映射(getRC 函数)
题目中编号的转行交替规则是核心难点,我们需要写一个辅助函数 getRC,输入方格编号 id 和棋盘大小 n,输出该编号对应的二维数组坐标(r,c)。
映射逻辑推导(结合示例理解):
-
先计算当前编号所在的“层”(从下往上数的行数):r_raw = Math.floor((id - 1) / n)。比如 id=1,n=2,(1-1)/2=0,即第 0 层(最下层);id=3,(3-1)/2=1,即第 1 层(上层)。
-
计算在当前层内的列索引(从左到右):c_raw = (id - 1) % n。比如 id=2,(2-1)%2=1,即第 1 列(从左数)。
-
处理转行交替方向:如果当前层 r_raw 是奇数(上层),则列索引反转(从右到左),即 c = n - 1 - c_raw;如果是偶数(下层),则列索引不变。
-
将“从下往上的层”转换为二维数组的行索引(从上往下数):原数组的行索引 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。
六、常见错误与避坑点
-
坐标映射错误:忘记反转奇数层的列索引,或混淆“从下往上的层”与“原数组的行索引”,导致访问到错误的棋盘方格,进而计算错误。
-
重复访问:未使用 vis 数组,导致同一个方格被多次加入队列,陷入循环(比如蛇从 5 跳到 3,又从 3 跳回 5)。
-
跳转逻辑错误:多次跳转(比如跳到一个有梯子的方格后,又继续跳该梯子的目的地),违反题目规则。
-
终止条件判断错误:在处理完跳转后才判断是否到达目标,而不是在跳转前(虽然不影响结果,但会多做一次判断,效率略低)。
梳理完避坑点,我们对这道题做一个完整总结,帮助大家巩固核心知识点。
七、总结
蛇梯棋这道题,核心是用 BFS 求解最短路径,难点在于理解棋盘编号规则并实现正确的坐标映射。解题流程可以总结为:
-
明确问题本质:最短路径 → BFS 求解;
-
解决核心难点:实现编号与坐标的映射(getRC 函数);
-
处理边界条件:蛇/梯子的跳转规则、访问标记、终止条件;
-
代码优化:根据语言特性调整队列操作,提升效率。
只要掌握了 BFS 的核心思想,再理清坐标映射的逻辑,这道题就能轻松 AC。如果在做题过程中遇到坐标映射错误,可以多举几个小例子手动计算,加深理解。