1.题目描述
让我们一起来玩扫雷游戏!
给你一个大小为 m x n
二维字符矩阵 board
,表示扫雷游戏的盘面,其中:
'M'
代表一个 未挖出的 地雷,'E'
代表一个 未挖出的 空方块,'B'
代表没有相邻(上,下,左,右,和所有4个对角线)地雷的 已挖出的 空白方块,- 数字(
'1'
到'8'
)表示有多少地雷与这块 已挖出的 方块相邻, 'X'
则表示一个 已挖出的 地雷。
给你一个整数数组 click
,其中 click = [clickr, clickc]
表示在所有 未挖出的 方块('M'
或者 'E'
)中的下一个点击位置(clickr
是行下标,clickc
是列下标)。
根据以下规则,返回相应位置被点击后对应的盘面:
- 如果一个地雷(
'M'
)被挖出,游戏就结束了- 把它改为'X'
。 - 如果一个 没有相邻地雷 的空方块(
'E'
)被挖出,修改它为('B'
),并且所有和其相邻的 未挖出 方块都应该被递归地揭露。 - 如果一个 至少与一个地雷相邻 的空方块(
'E'
)被挖出,修改它为数字('1'
到'8'
),表示相邻地雷的数量。 - 如果在此次点击中,若无更多方块可被揭露,则返回盘面。
示例 1:
输入:board = [["E","E","E","E","E"],["E","E","M","E","E"],["E","E","E","E","E"],["E","E","E","E","E"]], click = [3,0] 输出:[["B","1","E","1","B"],["B","1","M","1","B"],["B","1","1","1","B"],["B","B","B","B","B"]]
示例 2:
输入:board = [["B","1","E","1","B"],["B","1","M","1","B"],["B","1","1","1","B"],["B","B","B","B","B"]], click = [1,2] 输出:[["B","1","E","1","B"],["B","1","X","1","B"],["B","1","1","1","B"],["B","B","B","B","B"]]
提示:
m == board.length
n == board[i].length
1 <= m, n <= 50
board[i][j]
为'M'
、'E'
、'B'
或数字'1'
到'8'
中的一个click.length == 2
0 <= clickr < m
0 <= clickc < n
board[clickr][clickc]
为'M'
或'E'
来源:力扣(LeetCode)
链接:. - 力扣(LeetCode)
2.题解
鄙人只是算法小白,目前只会BFS,DFS正在努力学习中...
class Solution {
int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, -1}, {1, 1}, {-1, 1}, {-1, -1}};//八个方向变化量数组
public char[][] updateBoard(char[][] board, int[] click) {
int r = board.length, c = board[0].length;
//检测点击位置是否为雷
if(board[click[0]][click[1]] == 'M') {
board[click[0]][click[1]] = 'X';
return board;
}
//统计雷的数量并加以操作
Queue<int[]> queue = new LinkedList<>();
//查重数组
boolean[][] test = new boolean[board.length][board[0].length];
queue.offer(new int[]{click[0], click[1]});
//设置为已经查找过
test[click[0]][click[1]] = true;
while(!queue.isEmpty()) {
int num = 0;
int[] temp = queue.poll();
for(int[] direction : directions) {
int row = temp[0] + direction[0], col = temp[1] + direction[1];
if(row >= 0 && col >= 0 && row < r && col < c && !test[row][col] && board[row][col] == 'M') {
//检测到一个雷
++num;
}
}
if(num > 0) {
//将雷的数量换到当前位置
board[temp[0]][temp[1]] = (char)(num + '0');
}else {
//如果没有检测到雷,将四周没有越界并且没有查找过的位置全部入队
for(int[] direction : directions) {
int row = temp[0] + direction[0], col = temp[1] + direction[1];
if(row >= 0 && col >= 0 && row < r && col < c && !test[row][col] && board[row][col] == 'E') {
queue.offer(new int[]{row, col});
test[row][col] = true;
}
}
//没有在当前位置查找到雷,按照题目要求设置为字符"B"
board[temp[0]][temp[1]] = 'B';
}
}
return board;
}
}
使用:广度优先搜索
3.题目思路与代码讲解
看到这道题的示例一时,我们可以发现一个现象:
相信各位可以清楚看到在返回数组的可视化中,一个“另类”非常夺目,即数组位置row=0,col=2的方格竟然没有被查找。这就意味着进行BFS后将元素向八方遍历后检测到的雷的数量进行替换的思路行不通,不然row=0,col=2的位置也会有一个显眼的1,这明显不对。所以这可以告诉我们在一个元素的八个方向检测到至少一个雷时,就不要再将周围八个方向的元素入队。这也避免了将重复元素坐标入队和将雷坐标入队导致BFS执行错误的的bug
所以按照此路线,我们可以慢慢来梳理我们的解答思路:
1. 检测点击位置是否为雷,如果是,直接修改输入数组并返回
2. 如果代码执行到了这一步,就说明点击位置不是雷,此时可以以点击位置为中心进行BFS,并且考虑到可能会发生重复检测即一个数组元素被检测两次或更多导致超时,有必要使用官方给出的"BFS模板2",但是Set集合在后面处理数据多了的时候会有非常多的元素,这会让contains方法所用的时间增加,从而导致超时,所以我们有必要使用一个boolean[][],其中元素与board中元素一一对应,当boolean[][]中某个位置上的值为true时,就代表这个位置对应的board元素已经被搜索过一次。在进行BFS时,需要记录当前元素在八个方向(上,下,左,右,左上,左下,右上,右下)出现雷的次数,以便当检测到雷时可以及时调用
3. 进行一个分支,前面我们提到:在一个元素的八个方向检测到至少一个雷时,就不要再将周围八个方向的元素入队。所以一旦检测到雷的次数不为0时(就是周围有至少一个雷),将该值直接替换掉当前坐标上的元素,否则将当前元素标记为"B",之后将这个元素周围八个方向的没有越界并且没有搜索过的元素全部入队即可
我们可以按着这个思路理解代码了。
int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, -1}, {1, 1}, {-1, 1}, {-1, -1}};//八个方向变化量数组
此处代码定义了八个方向的row,col值变化量数组,调用时,只需要将当前坐标加上对应的row,col变化量即可得到该坐标向一个方向移动一次的坐标,可谓简便至极
int r = board.length, c = board[0].length;
//检测点击位置是否为雷
if(board[click[0]][click[1]] == 'M') {
board[click[0]][click[1]] = 'X';
return board;
}
这里正好对应了思路列表中的1,看看“游玩者”的运气有没有那么差直接一次踩雷
//统计雷的数量并加以操作
Queue<int[]> queue = new LinkedList<>();
//查重数组
boolean[][] test = new boolean[board.length][board[0].length];
queue.offer(new int[]{click[0], click[1]});
//设置为已经查找过
test[click[0]][click[1]] = true;
while(!queue.isEmpty()) {
int num = 0;
int[] temp = queue.poll();
for(int[] direction : directions) {
int row = temp[0] + direction[0], col = temp[1] + direction[1];
if(row >= 0 && col >= 0 && row < r && col < c && !test[row][col] && board[row][col] == 'M') {
//检测到一个雷
++num;
}
}
if(num > 0) {
//将雷的数量换到当前位置
board[temp[0]][temp[1]] = (char)(num + '0');
}else {
//如果没有检测到雷,将四周没有越界并且没有查找过的位置全部入队
for(int[] direction : directions) {
int row = temp[0] + direction[0], col = temp[1] + direction[1];
if(row >= 0 && col >= 0 && row < r && col < c && !test[row][col] && board[row][col] == 'E') {
queue.offer(new int[]{row, col});
test[row][col] = true;
}
}
//没有在当前位置查找到雷,按照题目要求设置为字符"B"
board[temp[0]][temp[1]] = 'B';
}
}
return board;
这里是整体实现部分包括了思路中的2和3步,这里分开来讲
//统计雷的数量并加以操作
Queue<int[]> queue = new LinkedList<>();
//查重数组
boolean[][] test = new boolean[board.length][board[0].length];
queue.offer(new int[]{click[0], click[1]});
//设置为已经查找过
test[click[0]][click[1]] = true;
while(!queue.isEmpty()) {
int num = 0;
int[] temp = queue.poll();
for(int[] direction : directions) {
int row = temp[0] + direction[0], col = temp[1] + direction[1];
if(row >= 0 && col >= 0 && row < r && col < c && !test[row][col] && board[row][col] == 'M') {
//检测到一个雷
++num;
}
}
//下面略
}
//略
这里对应了思路列表中的第二步
可以看到,这里定义了队列和查重数组,test[click[0]][click[1]] = true;的目的是将当前点击位置设置为“已经查找”,即将查重数组中同样位置的值置为true
后面的row和col是经过方向枚举后新的元素位置的行坐标与纵坐标,但是有可能越界,所以下面使用了一个老长的if进行判断,同时进行查重,如果新的坐标位置没有越界,没有搜索过,并且是个雷,就将记录雷的数量的num变量自增,所以这部分代码实现了思路2的功能,将BFS搜索到的每一个元素都进行记录临近的雷的数量。那么这个数量(num中存储的值)有什么用呢?接下来就是处理数据的第三部分代码
if(num > 0) {
//将雷的数量换到当前位置
board[temp[0]][temp[1]] = (char)(num + '0');
}else {
//如果没有检测到雷,将四周没有越界并且没有查找过的位置全部入队
for(int[] direction : directions) {
int row = temp[0] + direction[0], col = temp[1] + direction[1];
if(row >= 0 && col >= 0 && row < r && col < c && !test[row][col] && board[row][col] == 'E') {
queue.offer(new int[]{row, col});
test[row][col] = true;
}
}
//没有在当前位置查找到雷,按照题目要求设置为字符"B"
board[temp[0]][temp[1]] = 'B';
}
这部分代码实现了思路列表中3的功能,正如思路3所说:进行一个分支,在一个元素的八个方向检测到至少一个雷时,就不要再将周围八个方向的元素入队。所以一旦检测到雷的次数不为0时(就是周围有至少一个雷),将该值直接替换掉当前坐标上的元素,否则将当前元素标记为"B",之后将这个元素周围八个方向的没有越界并且没有搜索过的元素全部加入队列即可
当元素周围没有雷时,将其标记为"B"并且将其周围没有被查找并且没有越界的坐标全部入队,一旦检测到了雷,就将雷的数量写在当前元素的位置
但是,写入就写入,这个:
board[temp[0]][temp[1]] = (char)(num + '0');
后面的东西什么鬼?这是一种快捷将数字转换为字符的方法,例如将5转换为'5',前者是int值,而后者是字符。
原理是利用了ascii中0到9是连续排列的,对应ascii值48到57,如果将字符强制转换为数字,那么得到的只是这个字符的ascii值,即'0'转换为int值不是0而是0的ascii值48,因此将这个数字值加上0的ascii不就可以得到这个数字本体的ascii了吗?再把这个ascii转换为char类型不就是我们要转换为char的数字了吗?
例子:将1这个int值转为char
0的ascii是48,48+1=49,正好是字符'1'的ascii,再将49强制转换为char,就是1
4.复杂度分析
设扫雷板的行数为r,列数为c,那么最坏情况下,所有的格子都需要被访问一次
所以时间复杂度为 O(r * c)
算法使用了一个队列来存储待访问的格子,以及一个布尔数组来记录已访问过的格子。所以空间复杂度为O(r * c),与扫雷板的大小相关
5.结语
感谢您的观看与支持!这只是鄙人对这道题的解法,肯定不是此题最优解,但还是能感谢您能抽出时间来看我的小破文章!
如有错误感谢在评论区中指出!