文章

[LeetCode] 每日一题 909. 蛇梯棋(BFS)

题目描述

给你一个大小为 n x n 的整数矩阵 board ,方格按从 1n2 编号,编号遵循 转行交替方式 从左下角开始 (即,从 board[n - 1][0] 开始)的每一行改变方向。

你一开始位于棋盘上的方格  1。每一回合,玩家需要从当前方格 curr 开始出发,按下述要求前进:

  • 选定目标方格 next ,目标方格的编号在范围 [curr + 1, min(curr + 6, n2)]

    • 该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。

  • 传送玩家:如果目标方格 next 处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格 next 。 

  • 当玩家到达编号 n2 的方格时,游戏结束。

如果 board[r][c] != -1 ,位于 rc 列的棋盘格中可能存在 “蛇” 或 “梯子”。那个蛇或梯子的目的地将会是 board[r][c]。编号为 1n2 的方格不是任何蛇或梯子的起点。

注意,玩家在每次掷骰的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。

  • 举个例子,假设棋盘是 [[-1,4],[-1,3]] ,第一次移动,玩家的目标方格是 2 。那么这个玩家将会顺着梯子到达方格 3 ,但 不能 顺着方格 3 上的梯子前往方格 4 。(简单来说,类似飞行棋,玩家掷出骰子点数后移动对应格数,遇到单向的路径(即梯子或蛇)可以直接跳到路径的终点,但如果多个路径首尾相连,也不能连续跳多个路径)

返回达到编号为 n2 的方格所需的最少掷骰次数,如果不可能,则返回 -1

题目链接

https://leetcode.cn/problems/snakes-and-ladders

示例输入

示例 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]

  • 编号为 1n2 的方格上没有蛇或梯子

解题思路

这题是一个典型的 最短步数搜索问题,目标是从编号 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,这里也是一样的套路,只不过这次要对编号的映射再动动脑筋 😂

希望这篇分享能为你带来启发!如果你有任何问题或建议,欢迎在评论区留言,与我共同交流探讨。

License:  CC BY 4.0