[LeetCode] 每日一题 909. 蛇梯棋(BFS)
题目描述
给你一个大小为 n x n
的整数矩阵 board
,方格按从 1
到 n2
编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 board[n - 1][0]
开始)的每一行改变方向。
你一开始位于棋盘上的方格 1
。每一回合,玩家需要从当前方格 curr
开始出发,按下述要求前进:
选定目标方格
next
,目标方格的编号在范围[curr + 1, min(curr + 6, n2)]
。该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。
传送玩家:如果目标方格
next
处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格next
。当玩家到达编号
n2
的方格时,游戏结束。
如果 board[r][c] != -1
,位于 r
行 c
列的棋盘格中可能存在 “蛇” 或 “梯子”。那个蛇或梯子的目的地将会是 board[r][c]
。编号为 1
和 n2
的方格不是任何蛇或梯子的起点。
注意,玩家在每次掷骰的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。
举个例子,假设棋盘是
[[-1,4],[-1,3]]
,第一次移动,玩家的目标方格是2
。那么这个玩家将会顺着梯子到达方格3
,但 不能 顺着方格3
上的梯子前往方格4
。(简单来说,类似飞行棋,玩家掷出骰子点数后移动对应格数,遇到单向的路径(即梯子或蛇)可以直接跳到路径的终点,但如果多个路径首尾相连,也不能连续跳多个路径)
返回达到编号为 n2
的方格所需的最少掷骰次数,如果不可能,则返回 -1
。
题目链接
示例输入
示例 1
输入:board = [[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,35,-1,-1,13,-1],[-1,-1,-1,-1,-1,-1],[-1,15,-1,-1,-1,-1]]
输出:4
解释:
首先,从方格 1 [第 5 行,第 0 列] 开始。
先决定移动到方格 2 ,并必须爬过梯子移动到到方格 15 。
然后决定移动到方格 17 [第 3 行,第 4 列],必须爬过蛇到方格 13 。
接着决定移动到方格 14 ,且必须通过梯子移动到方格 35 。
最后决定移动到方格 36 , 游戏结束。
可以证明需要至少 4 次移动才能到达最后一个方格,所以答案是 4 。
示例 2
输入:board = [[-1,-1],[-1,3]]
输出:1
提示
n == board.length == board[i].length
2 <= n <= 20
board[i][j]
的值是-1
或在范围[1, n^2]
内编号为
1
和n2
的方格上没有蛇或梯子
解题思路
这题是一个典型的 最短步数搜索问题,目标是从编号 1 的格子,走到编号 n² 的格子,并考虑“蛇”和“梯子”的跳跃行为。第一眼就能想到用 BFS,用队列一层层搜索出当前所有可能的点,直到找到目标点。
不过,这题的特别之处在于棋盘的编号方式是 “之”字型的、从左下到右上交错编号,而不是平铺式地顺序编号。这意味着我们在进行跳转判断(是否是蛇或梯子)时,不能直接通过编号找数组位置,而需要维护一个从 编号到坐标的映射函数。所以我的做法是用一个函数 getPos(n, id)
来处理这个映射,把编号转成二维数组的行列坐标,再去判断该位置是否存在跳跃路径。
除此之外,还有一个坑点就是:梯子或蛇最多只能触发一次,哪怕目标位置又是另一个梯子的起点,也不能继续跳。这也和 BFS 的天然机制契合 —— 我们每次从编号 curr
出发,只会处理一次跳跃逻辑。
总体来说,这题的处理方式和我之前做的一些图论题(比如广度优先搜索求最短路径)如出一辙,数据量不大,只需要注意转换规则和跳跃逻辑的细节就好。
代码实现
class Solution {
public int snakesAndLadders(int[][] board) {
int n = board.length;
boolean[] visited = new boolean[n * n + 1];
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[] {1, 0});
while (!queue.isEmpty()) {
int[] info = queue.poll();
for (int i = 1; i <= 6; i++) {
int next = info[0] + i;
if (next > n * n) {
break;
}
int[] nextPos = getPos(n, next);
if (board[nextPos[0]][nextPos[1]] != -1) {
next = board[nextPos[0]][nextPos[1]];
}
if (next == n * n) {
return info[1] + 1;
}
if (!visited[next]) {
visited[next] = true;
queue.offer(new int[] {next, info[1] + 1});
}
}
}
return -1;
}
private int[] getPos(int n, int id) {
int row = (id - 1) / n;
int col = (id - 1) % n;
if (row % 2 == 1) {
col = n - 1 - col;
}
return new int[] {n - 1 - row, col};
}
}
复杂度分析
时间复杂度:O(n²)
最多访问每个编号一次,每个编号最多尝试 6 个方向 🎲
空间复杂度:O(n²)
主要是 BFS 队列和 visited 数组的空间开销 📦
总结
这题让我再次体会到 BFS 在“最短路径类问题”中的高效与稳定,同时也印证了一个图论题的通用思路 —— 当题目自带数据结构不适合时,构建一个自己的抽象层(如编号转坐标)会更好地简化后续操作。
我之前做树的题时也有类似的思路,比如把边转成邻接表,再去 DFS 或 BFS,这里也是一样的套路,只不过这次要对编号的映射再动动脑筋 😂
希望这篇分享能为你带来启发!如果你有任何问题或建议,欢迎在评论区留言,与我共同交流探讨。