给你一个大小为 n x n
的整数矩阵 board
,方格按从 1
到 n2
编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 board[n - 1][0]
开始)每一行交替方向。
玩家从棋盘上的方格 1
(总是在最后一行、第一列)开始出发。
每一回合,玩家需要从当前方格 curr
开始出发,按下述要求前进:
- 选定目标方格
next
,目标方格的编号符合范围[curr 1, min(curr 6, n2)]
。- 该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。
- 传送玩家:如果目标方格
next
处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格next
。 - 当玩家到达编号
n2
的方格时,游戏结束。
r
行 c
列的棋盘,按前述方法编号,棋盘格中可能存在 “蛇” 或 “梯子”;如果 board[r][c] != -1
,那个蛇或梯子的目的地将会是 board[r][c]
。编号为 1
和 n2
的方格上没有蛇或梯子。
注意,玩家在每回合的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。
- 举个例子,假设棋盘是
[[-1,4],[-1,3]]
,第一次移动,玩家的目标方格是2
。那么这个玩家将会顺着梯子到达方格3
,但 不能 顺着方格3
上的梯子前往方格4
。
返回达到编号为 n2
的方格所需的最少移动次数,如果不可能,则返回 -1
。
示例 1:
代码语言:javascript复制输入: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:
代码语言:javascript复制输入:board = [[-1,-1],[-1,3]]
输出:1
提示:
n == board.length == board[i].length
2 <= n <= 20
grid[i][j]
的值是-1
或在范围[1, n2]
内- 编号为
1
和n2
的方格上没有蛇或梯子
题目分析 这道题要求从起点(编号为 1 的格子)到终点(编号为 n^2 的格子)的最短路径。 路径搜索有两种方法:深度优先与广度优先。由于这道题要找到的是到达终点时的最短路径,因此 用广度优先搜索更方便些。【广度优先搜索就是每次把离当前节点最近的节点作为待搜索的节点】
转移方向 这道题和传统的矩阵路径搜索不一样的是,它的下一个搜索方格不是相邻方格,而是下6个编号。即如果当前处理的方格编号为 curr,那么其可以转移到编号属于 [curr 1, min(curr 6, n2)] 的方格里。
其次,还有第二个转移规则,如果这个方格编号 可以传送 (board[r][c] != -1),那么就会 传送到指定位置。即假设编号为 next 的单元格所在位置为 (r,c),那么玩家将不会转移到 next 编号的方格,而是转移到 board[r][c] 编号的方格。
根据编号确定方格位置 那么现在出现了一个问题,如何根据编号确定方格的位置,即根据 i 确定其所在的 r 和 c。 传统的矩阵编号和单元格位置的关系如下图:
而这道题有三个特殊的地方:
矩阵编号从 1 开始,而不是从 0 开始。因此计算行和列要先对编号 -1,即 i - 1;
其次,行的排列是倒序的【或者说翻转了】,即原本的 r=0 跑到了 r=n-1,相当于从 n-1 行倒着往回数,因此计算出来的 r' = n - 1 - r;
最后,列的排列是蛇形的:原本我们每一列的排序都是从左到右的,因此计算出来的 c 是哪一列就是哪一列;但是现在我们从最后一行到首行的元素排列顺序是交替的:最后一行从左到右,倒数第二行从右到左,...:
从左到右的排列还是和原来的计算方式一致;而从右到左排列的那么列编号就是从 n-1 往回数,即 c = n-1-c;
由于是交替的,我们把行倒着编码(最后一行当成第 0 行,倒数第二行为 1 行,即 r 行的编号变成 n-1-r'),那么偶数行是从左到右,c' = 0 c【从首列0往右数c个位置】;奇数行是从右到左 c' = n-1-c【从最后一列n-1往左数c个位置】。
通过数学计算,我们可以得到实际的列 c' 与 行 r 的关系
偶数行 (n-1-r)& 1 = 0 奇数行 (n-1-r) & 1 = 1 记 x = (n-1-r)& 1 当 x = 0, 偶数行,c' = c; 当 x = 1, 奇数行,c' = n - 1 - c; 根据两点式直线方程计算方式,设 c' = kx b x = 0, 0 * k b = c x = 1, 1 * k b = n-1-c c' = (n-1-2c)x c = (n-1-2c) * (n-1-r)& 1 c 广度优先搜索 通过上两步,明确了下一个转移的编号。剩下的就是根据 广度优先搜索 借助队列对起点到终点的路径进行搜索。
如果能够到达终点,到达的时候一定是最短路径,直接返回操作数; 如果不能到达终点,即返回 -1。 代码 细节处理
队列中是同时存储了待搜索的方格编号和到达该方格时的最少移动数。 当然也可以只存储方格编号,那么搜索过程就类似 二叉树的层序遍历。每一次循环之前先获取队列中有多少元素,这些元素就是满足当前统计的距离/移动数的节点。我们只处理这么多个元素,剩下的元素都是新加入,都是下一个距离的元素。
代码语言:javascript复制class Solution {
public:
int snakesAndLadders(vector<vector<int>>& board) {
int n = board.size(); // 获取方阵的边长
int target = n * n; // 获取方阵尺寸,也是最后要到达目的地
queue<pair<int, int>> queue_; // 队列用于BFS,存放待搜索的方格编号和到达该方格时的最少移动数
queue_.emplace(1, 0); // 初始{1,0}入队,表示起点1,0次移动
vector<vector<bool>> visited(n, vector<bool>(n)); // 用于BFS过程中标记方格是否搜索过
// BFS
while(!queue_.empty()){
auto node = queue_.front();
queue_.pop();
int curr = node.first, cnt = node.second; // 获取当前搜索的方格宾浩和到达该方格的最少移动数
cnt ; // 移动数加1
for(int i = curr 1; i <= min(target, curr 6); i ){
// 枚举所有下一个可搜索且未搜索过的方格编号
int r = n-1 - (i-1) / n, c = (i-1) % n; // 根据方格编号获取这个编号的行和列
c = (n - 1 - 2 * c) * ((n-1-r) & 1); // 根据行数修正列数
if(visited[r][c])continue; // 跳过搜索过的编号
visited[r][c] = true; // 标记该编号已搜索
int next = board[r][c] == - 1 ? i : board[r][c]; // 如果这个编号所在的方格可以转移到其他格子,转移到对应编号;否则就是在当前编号
if(next == target)return cnt; // 到达终点,直接返回最小移动数
queue_.emplace(next, cnt); // 加入队列
}
}
return -1; // 退出循环说明没有到达目的地
}
};