思考过程:
分为三种情况,1.如果点击的是雷,修改为‘X’然后返回;2.如果点击的是和雷相连的点,找到这个点周围的雷数量k,修改为‘k’然后返回;3.如果点击的是空白方块,修改为‘B’,然后广度优先处理这个点周围的点,并记录下处理过的点。
代码实现:
char unknownMineC = 'M';//未挖出的地雷
char mineC = 'X';//挖出的地雷
char unknownC = 'E';//未挖出的空方块
char emptyC = 'B';//挖出的空白方块
char[] numbersC = {'1', '2', '3', '4', '5', '6', '7', '8'};
public char[][] updateBoard(char[][] board, int[] click) {
int x = click[0];
int y = click[1];
//地雷被挖出
if (board[x][y] == unknownMineC) {
board[x][y] = mineC;
return board;
}
//获取点击的位置,有相连地雷的数量
List<int[]> list = getLinkedPosition(board, click);
int num = getMineNum(board, list);
//有1个以上地雷与点击位置相连,该位置值改为地雷数,返回
if (num > 0) {
board[x][y] = numbersC[num - 1];
return board;
}
//没有地雷与点击位置相连,该位置值改为挖出的空白方块,并处理所有与该点相连的点
board[x][y] = emptyC;
Set<String> history = new HashSet<>();//用于记录已经处理过的位置
history.add(x + "-" + y);
return handle(board, list, history);
}
//输入n个位置,返回这些位置中有雷的个数
private int getMineNum(char[][] board, List<int[]> list) {
int num = 0;
for (int[] p : list) {
char c = board[p[0]][p[1]];
if (unknownMineC == c || mineC == c) {
num++;
}
}
return num;
}
//输入与空白方块相连的点,判断这些点的其他相连点,是否与雷相连,
// 如果相连,点的值修改为雷个数,如果不相连,点的值修改为空白方块,且递归处理与这个点相连的其他点
private char[][] handle(char[][] board, List<int[]> positions, Set<String> history) {
List<int[]> emptyList = new ArrayList<>();
for (int[] p : positions) {
if (!history.contains(p[0] + "-" + p[1])) {
history.add(p[0] + "-" + p[1]);
List<int[]> list = getLinkedPosition(board, p);
int num = getMineNum(board, list);
if (num > 0) {
//有n个地雷相连,修改值为n
board[p[0]][p[1]] = numbersC[num - 1];
} else {
//没有地雷与输入位置相连,所有相连点需要再次判断
board[p[0]][p[1]] = emptyC;
emptyList.addAll(list);
}
}
}
if (emptyList.size() == 0) {
return board;
} else return handle(board, emptyList, history);
}
//输入位置,返回与此位置相连的所有位置(3-8个)
private List<int[]> getLinkedPosition(char[][] board, int[] p) {
int x = p[0];
int y = p[1];
int maxX = board.length;
int maxY = board[0].length;
List<int[]> list = new ArrayList<>();
if (x - 1 >= 0) {
list.add(new int[]{x - 1, y});
}
if (y - 1 >= 0) {
list.add(new int[]{x, y - 1});
}
if (x - 1 >= 0 && y - 1 >= 0) {
list.add(new int[]{x - 1, y - 1});
}
if (x - 1 >= 0 && y + 1 < maxY) {
list.add(new int[]{x - 1, y + 1});
}
if (x + 1 < maxX) {
list.add(new int[]{x + 1, y});
}
if (y + 1 < maxY) {
list.add(new int[]{x, y + 1});
}
if (x + 1 < maxX && y - 1 >= 0) {
list.add(new int[]{x + 1, y - 1});
}
if (x + 1 < maxX && y + 1 < maxY) {
list.add(new int[]{x + 1, y + 1});
}
return list;
}
执行结果:
耗时太高了。。。思路应该是没有太大问题,可能在代码实现上应该优化。
算法复杂度分析:
时间复杂度:O(n*m),其中 n,m是行数和列数。
空间复杂度:O(n*m),其中 n,m是行数和列数。
官方提供了深度优先和广度优先两种解法
先看广度优先代码,代码如下:
int[] dirX = {0, 1, 0, -1, 1, 1, -1, -1};
int[] dirY = {1, 0, -1, 0, 1, -1, 1, -1};
public char[][] updateBoard(char[][] board, int[] click) {
int x = click[0], y = click[1];
if (board[x][y] == 'M') {
// 规则 1
board[x][y] = 'X';
} else{
bfs(board, x, y);
}
return board;
}
public void bfs(char[][] board, int sx, int sy) {
Queue<int[]> queue = new LinkedList<int[]>();
boolean[][] vis = new boolean[board.length][board[0].length];
queue.offer(new int[]{sx, sy});
vis[sx][sy] = true;
while (!queue.isEmpty()) {
int[] pos = queue.poll();
int cnt = 0, x = pos[0], y = pos[1];
for (int i = 0; i < 8; ++i) {
int tx = x + dirX[i];
int ty = y + dirY[i];
if (tx < 0 || tx >= board.length || ty < 0
|| ty >= board[0].length) {
continue;
}
// 不用判断 M,因为如果有 M 的话游戏已经结束了
if (board[tx][ty] == 'M') {
++cnt;
}
}
if (cnt > 0) {
// 规则 3
board[x][y] = (char) (cnt + '0');
} else {
// 规则 2
board[x][y] = 'B';
for (int i = 0; i < 8; ++i) {
int tx = x + dirX[i];
int ty = y + dirY[i];
// 这里不需要在存在 B 的时候继续扩展,因为 B 之前被点击的时候已经被扩展过了
if (tx < 0 || tx >= board.length || ty < 0
|| ty >= board[0].length || board[tx][ty] != 'E'
|| vis[tx][ty]) {
continue;
}
queue.offer(new int[]{tx, ty});
vis[tx][ty] = true;
}
}
}
}
一如既往的简洁高效,也一如既往的难理解。。。
深度优先代码:
int[] dirX = {0, 1, 0, -1, 1, 1, -1, -1};
int[] dirY = {1, 0, -1, 0, 1, -1, 1, -1};
public char[][] updateBoard(char[][] board, int[] click) {
int x = click[0], y = click[1];
if (board[x][y] == 'M') {
// 规则 1
board[x][y] = 'X';
} else {
dfs(board, x, y);
}
return board;
}
public void dfs(char[][] board, int x, int y) {
//找到与输入点相连的8个点(可能小于8)
int cnt = 0;
for (int i = 0; i < 8; ++i) {
int tx = x + dirX[i];
int ty = y + dirY[i];
if (tx < 0 || tx >= board.length || ty < 0 || ty >= board[0].length) {
continue;
}
// 不用判断 X,因为如果有 X 的话游戏已经结束了
if (board[tx][ty] == 'M') {
++cnt;
}
}
//判断这8个点中雷的数量,如果有雷,执行规则3,结束
if (cnt > 0) {
// 规则 3
board[x][y] = (char) (cnt + '0');
} else {
//如果没有雷,递归处理每一个点
// 规则 2
board[x][y] = 'B';
for (int i = 0; i < 8; ++i) {
int tx = x + dirX[i];
int ty = y + dirY[i];
// 这里不需要在存在 B 的时候继续扩展,因为 B 之前被点击的时候已经被扩展过了
if (tx < 0 || tx >= board.length || ty < 0 || ty >= board[0].length
|| board[tx][ty] != 'E') {
continue;
}
dfs(board, tx, ty);
}
}
}
借鉴:
对比官方提供的代码,我觉得可以借鉴的地方有如下:
1.使用两个数组(int[] dirX 和 int[] dirY)表示周围可能的8个点,在找周围点时,代码可以更优雅和高效。
2.使用char的值来判断点是否处理过,没有处理过的点值为'E',而不用另外再用个Set,增加多余的消耗。
3.做深度优先处理时,只需要传int[][]到方法中处理,不用返回int[][],传进来的那个对象,值已经被方法改变了。
借鉴后代码如下:
char unknownMineC = 'M';//未挖出的地雷
char mineC = 'X';//挖出的地雷
char unknownC = 'E';//未挖出的空方块
char emptyC = 'B';//挖出的空白方块
char[] numbersC = {'1', '2', '3', '4', '5', '6', '7', '8'};
int[] dirX0 = {0, 1, 0, -1, 1, 1, -1, -1};
int[] dirY0 = {1, 0, -1, 0, 1, -1, 1, -1};
/*
执行用时:26 ms, 在所有 Java 提交中击败了5.94%的用户
内存消耗:40.1 MB, 在所有 Java 提交中击败了44.39%的用户
* */
public char[][] updateBoard1(char[][] board, int[] click) {
int x = click[0];
int y = click[1];
//地雷被挖出
if (board[x][y] == unknownMineC) {
board[x][y] = mineC;
return board;
}
//获取点击的位置,有相连地雷的数量
List<int[]> list = getLinkedPosition(board, click);
int num = getMineNum(board, list);
//有1个以上地雷与点击位置相连,该位置值改为地雷数,返回
if (num > 0) {
board[x][y] = numbersC[num - 1];
return board;
}
//没有地雷与点击位置相连,该位置值改为挖出的空白方块,并处理所有与该点相连的点
board[x][y] = emptyC;
handle(board, list);
return board;
}
//输入n个位置,返回这些位置中有雷的个数
private int getMineNum(char[][] board, List<int[]> list) {
int num = 0;
for (int[] p : list) {
char c = board[p[0]][p[1]];
if (unknownMineC == c || mineC == c) {
num++;
}
}
return num;
}
//输入与空白方块相连的点,判断这些点的其他相连点,是否与雷相连,
// 如果相连,点的值修改为雷个数,如果不相连,点的值修改为空白方块,且递归处理与这个点相连的其他点
private void handle(char[][] board, List<int[]> positions) {
List<int[]> emptyList = new ArrayList<>();
for (int[] p : positions) {
//使用char的值来判断是否处理过,为’E‘时才是没有处理过的
if (board[p[0]][p[1]] == unknownC) {
List<int[]> list = getLinkedPosition(board, p);
int num = getMineNum(board, list);
if (num > 0) {
//有n个地雷相连,修改值为n
board[p[0]][p[1]] = numbersC[num - 1];
} else {
//没有地雷与输入位置相连,所有相连点需要再次判断
board[p[0]][p[1]] = emptyC;
emptyList.addAll(list);
}
}
}
if (emptyList.size() > 0) {
handle(board, emptyList);
}
}
//输入位置,返回与此位置相连的所有位置(3-8个)
private List<int[]> getLinkedPosition(char[][] board, int[] p) {
int x = p[0];
int y = p[1];
int maxX = board.length;
int maxY = board[0].length;
List<int[]> list = new ArrayList<>();
for (int i = 0; i < 8; ++i) {
int tx = x + dirX0[i];
int ty = y + dirY0[i];
if (tx < 0 || tx >= maxX || ty < 0 || ty >= maxY) {
continue;
}
list.add(new int[]{tx, ty});
}
return list;
}
执行结果:
有一定提升,和官方广度优先代码执行结果差不多。
总结:
同样的解题思路,实现出来的代码却可以有天壤之别。代码的简洁和高效上,还需要提高。