扫雷游戏 图解题解
这道题到底在问什么
- 输入
- 点中间空格,周围正好 1 颗雷
- 输出
- 该格变数字 1,不扩散
- 输入
- 点角落空格,周围 0 颗雷
- 输出
- 该格变 B,并把邻居一片翻开
先想最直接的笨办法
先把 (0,0) 周围在界内的邻居用橙色圈出来,一个一个看是不是雷,红色的才算一颗。(动画第 8 步)
最优解:为什么这么做
一句话答案:LeetCode 529 扫雷游戏:点到雷就改 X 结束;点到空格先数周围 8 格的雷,有雷填数字就停、没雷变 B 并递归揭开邻格,一次 DFS 铺开整片安全区,时间 O(m·n)。
点一下,盘面要变成什么样
给一张 m×n 的字符盘面 board:M 是没挖出的雷,E 是没挖出的空格,另给一次点击 click。点到雷(M)就改成 X、游戏结束;点到空格(E),先数它周围 8 格有几颗雷:有雷就改成对应数字、到此打住,一颗都没有就改成 B 并把周围没挖的格接着翻开,最后返回盘面。难点全在最后半句——一格空白会牵出一连串翻开,不是点哪翻哪。
为什么不能只把点中那格翻开
先只翻点中那一格看差在哪。若点到的空格周围没有雷,它要变 B,而 B 就意味着『邻格还得继续翻』,其中周围无雷的又会变 B、再往外带。只翻一格就停,等于把该展开的一大片安全区留在原地,盘面是错的。所以这题不是做一次判断,而是从点击点出发沿『无雷空格』一路蔓延,直到撞上贴着雷的格才收边——这种扎进去、走不通再退回来的遍历,就是深度优先搜索(DFS)。
空格的两条岔路,为什么只有 0 才扩散
把揭开一个空格拆成一把尺子:先数它周围 8 格(含 4 条对角线)的雷数 cnt。cnt 大于 0,说明紧挨着雷,填上 cnt 就停手——它是一道警戒线,再往外翻就踩进雷区。cnt 等于 0,周围一圈都安全,才标成 B、对没挖的邻格递归揭开。为什么偏偏 0 才扩散?因为数字格本身就在说『旁边有雷、别往这走』,点一下摊开一大片的边界,就是这些数字格圈出来的。
一次递归揭开,具体是怎么铺的
揭开某格 (i,j),用双层循环扫 x 从 i-1 到 i+1、y 从 j-1 到 j+1 这 9 个位置,每个先判越界(0≤x<m 且 0≤y<n),在界内且是 M 才计入 cnt。若 cnt 为 0,这圈里还没挖的空格(E)就递归揭开。循环也会扫到 (i,j) 自己,但它此刻是待处理的 E、不是 M,不会被误当雷,省了跳过自身的判断。一格一旦标成 B 或数字就不再是 E,不会二次揭开,递归也不打转。
拿一张 3×4 盘面点 (0,0) 走一遍
盘面 3 行 4 列,唯一的雷 M 在 (1,2),其余全是 E,点左上角 (0,0)。(0,0) 周围 (0,1)、(1,0)、(1,1) 都没雷,cnt=0,标 B 并把这三格排进递归。揭 (0,1):周围含 (1,2),数到 1 颗雷,填 1 停下。揭 (1,0):无雷变 B,带出 (1,1)、(2,0)、(2,1);(1,1) 挨着雷填 1,(2,0) 无雷变 B 又带出 (2,1),(2,1) 挨着雷填 1。最终最左一列是三个 B,紧邻一列三个 1,把那颗雷挡在右侧,共翻开 6 格。
复杂度,还有几处一漏就全盘皆错
每个格子最多被揭开一次,数周围 8 格是常数工作,时间 O(m·n);空间耗在递归栈,最坏整片盘面连成一块空白,栈深逼近格子总数,也是 O(m·n)。让结果全错的常是几处细节。空格不先数雷就变 B 往外扩,本该拦住洪水的数字墙没长出来,会冲穿整盘。数雷只顾上下左右 4 格漏了对角线,斜角的雷被算少,可相邻是含对角的 8 格。给填了数字的格再递归就越过警戒线,右侧不该翻的格也翻了。两种极端反而简单:空格周围无雷直接变 B,点下去是雷则一步改 X。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这把尺子:空格先数雷,有雷填数字就刹车,没雷变 B 接着冲。每一帧都在套它。
- 4红色 M = 没挖的雷;灰色 E = 没挖的空格这是初始盘面。红色的 M 是雷,在 (1,2);其余灰色的 E 都是还没挖开的空格。我们要演两种点法。
- 5点击落在 (1,2),翻开一看是 M先看简单的一条。假如这次点的正好是 (1,2),翻开一看是 M,一颗雷。
- 6雷被挖出,(1,2) 改为 X碰到雷不数邻居、不扩散,直接把它改成 X,游戏立刻结束,返回盘面。这就是规则一,一步到位。
- 7重新开一局,点击落在左上角 (0,0)重新开一局。这次点左上角 (0,0),它是空格,不是雷,于是从这里开始一次 DFS 揭开。
- 8圈出 (0,0) 在界内的 3 个邻居,逐格找雷 | 已揭开 0先把 (0,0) 周围在界内的邻居用橙色圈出来,一个一个看是不是雷,红色的才算一颗。
- 9看 (0,0) 的 8 个邻居,数到 0 颗雷 | 已揭开 0进入 (0,0)(紫色)。橙色圈出的是它在界内的邻居,挨个看一遍,数雷时只把标红的雷格算进去,这一格数到 0 颗雷。
- 10(0,0) 周围 0 颗雷,改成 B | 已揭开 1周围一颗雷都没有,于是把 (0,0) 改成 B 空白(变蓝)。空白格意味着要继续往外揭开。
- 11(0,0) 向 3 个没挖的邻居递归揭开 | 已揭开 1(0,0) 是空白,把它周围还没挖的格 (0,1)、(1,0)、(1,1)(橙色)逐个压进递归栈,接着一个个揭开它们。
- 12(0,1) 周围在界内共 5 格,只圈出仍未揭开的 4 个候选格/雷 | 已揭开 1(0,1) 周围在界内的格子共 5 个,屏幕只圈出仍未揭开的 4 个候选格/雷;已经揭开的 (0,0) 不是雷、不影响这次数雷计数。
- 13看 (0,1) 的 8 个邻居,数到 1 颗雷 | 已揭开 1轮到 (0,1),同样数它周围 8 格,这一回数到 1 颗雷。
- 14(0,1) 周围有雷,填成 1,不再扩散 | 已揭开 2周围雷数大于 0,所以 (0,1) 直接填成数字 1(变绿)。它像一道墙,挡住洪水,不再往外扩散。
- 15看 (1,0) 的 8 个邻居,数到 0 颗雷 | 已揭开 2轮到 (1,0),同样数它周围 8 格,这一回数到 0 颗雷。
- 16(1,0) 周围 0 颗雷,改成 B | 已揭开 3(1,0) 周围同样没雷,也变成 B,继续向外冲。
- 17(1,0) 向 3 个没挖的邻居递归揭开 | 已揭开 3(1,0) 是空白,把它周围还没挖的格 (1,1)、(2,0)、(2,1)(橙色)逐个压进递归栈,接着一个个揭开它们。
- 18看 (1,1) 的 8 个邻居,数到 1 颗雷 | 已揭开 3轮到 (1,1),同样数它周围 8 格,这一回数到 1 颗雷。
- 19(1,1) 周围有雷,填成 1,不再扩散 | 已揭开 4(1,1) 周围也有雷,填成 1 就刹车,这片墙又长出一格。
- 20看 (2,0) 的 8 个邻居,数到 0 颗雷 | 已揭开 4轮到 (2,0),同样数它周围 8 格,这一回数到 0 颗雷。
- 21(2,0) 周围 0 颗雷,改成 B | 已揭开 5(2,0) 周围同样没雷,也变成 B,继续向外冲。
- 22(2,0) 向 1 个没挖的邻居递归揭开 | 已揭开 5(2,0) 是空白,把它周围还没挖的格 (2,1)(橙色)逐个压进递归栈,接着一个个揭开它们。
- 23看 (2,1) 的 8 个邻居,数到 1 颗雷 | 已揭开 5轮到 (2,1),同样数它周围 8 格,这一回数到 1 颗雷。
- 24(2,1) 周围有雷,填成 1,不再扩散 | 已揭开 6(2,1) 周围也有雷,填成 1 就刹车,这片墙又长出一格。
- 25递归栈清空,左两列已揭开,右侧被数字墙挡住DFS 全部返回。最左一列是三个 B 空白,旁边一列是三个数字 1 组成的墙,墙把右侧没必要揭开的格(含那颗雷 M)挡在外面,这就是最终返回的盘面。
⚠️ 容易写错的地方
✗ 错:空格不数雷就直接变 B 扩散
✓ 对:先数周围 8 格的雷,有雷要填数字并停下
漏数会把本该当墙的数字格也揭开,洪水冲穿整盘,结果全错
✗ 错:只看上下左右 4 个方向
✓ 对:8 个方向都要数,含 4 个对角线
题目明确相邻包含对角线,漏掉对角会把雷数算少
✗ 错:对数字格继续递归扩散
✓ 对:雷数大于 0 的格填完数字立刻返回
数字格是边界墙,继续扩散就越过了雷的警戒线
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
def dfs(i: int, j: int):
cnt = 0
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if 0 <= x < m and 0 <= y < n and board[x][y] == "M":
cnt += 1
if cnt:
board[i][j] = str(cnt)
else:
board[i][j] = "B"
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if 0 <= x < m and 0 <= y < n and board[x][y] == "E":
dfs(x, y)
m, n = len(board), len(board[0])
i, j = click
if board[i][j] == "M":
board[i][j] = "X"
else:
dfs(i, j)
return boardC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
int m = board.size(), n = board[0].size();
int i = click[0], j = click[1];
function<void(int, int)> dfs = [&](int i, int j) {
int cnt = 0;
for (int x = i - 1; x <= i + 1; ++x) {
for (int y = j - 1; y <= j + 1; ++y) {
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'M') {
++cnt;
}
}
}
if (cnt) {
board[i][j] = cnt + '0';
} else {
board[i][j] = 'B';
for (int x = i - 1; x <= i + 1; ++x) {
for (int y = j - 1; y <= j + 1; ++y) {
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'E') {
dfs(x, y);
}
}
}
}
};
if (board[i][j] == 'M') {
board[i][j] = 'X';
} else {
dfs(i, j);
}
return board;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private char[][] board;
private int m;
private int n;
public char[][] updateBoard(char[][] board, int[] click) {
m = board.length;
n = board[0].length;
this.board = board;
int i = click[0], j = click[1];
if (board[i][j] == 'M') {
board[i][j] = 'X';
} else {
dfs(i, j);
}
return board;
}
private void dfs(int i, int j) {
int cnt = 0;
for (int x = i - 1; x <= i + 1; ++x) {
for (int y = j - 1; y <= j + 1; ++y) {
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'M') {
++cnt;
}
}
}
if (cnt > 0) {
board[i][j] = (char) (cnt + '0');
} else {
board[i][j] = 'B';
for (int x = i - 1; x <= i + 1; ++x) {
for (int y = j - 1; y <= j + 1; ++y) {
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'E') {
dfs(x, y);
}
}
}
}
}
}复杂度
时间
O(m·n)
每个格子最多被揭开一次,揭开时数周围 8 格是常数工作
空间
O(m·n)
最坏情况整片空白连成一块,递归栈深度可达格子总数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 扫雷游戏 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
用 DFS 和 BFS 做这题有区别吗?+
没有本质区别,两者最终揭开的格子集合完全一样,只是揭开的顺序不同。DFS 用递归写起来最短;但盘面很大、空白连成一大片时,递归可能压得很深,有栈溢出的风险。BFS 改用一个队列逐圈往外扩,深度可控,工程上更稳当。面试里把『两种都对,DFS 简洁、BFS 防爆栈』讲清楚就够了。
数周围 8 格时,把自己这一格也扫进去要紧吗?+
不要紧。参考代码用 x 从 i-1 到 i+1、y 从 j-1 到 j+1 的双层循环,会顺带扫到 (i,j) 自己。但此刻自己这一格正是待处理的空格 E、不是 M,所以不会被算成雷,计数结果不受影响,反而省掉了『跳过自身』的额外判断。想显式跳过自己也行,但没必要。
为什么数字格不能继续往外扩散?+
因为数字格代表『周围有雷』,它本身就是安全区和雷区之间的那道边界。真人玩扫雷时,点一下弹开一大片,四周总会围着一圈数字——那圈数字正是洪水停下的地方。如果对数字格也递归,就等于越过了这条警戒线,把紧贴着雷、本不该自动翻开的格子也翻了出来,展开范围整个错掉。所以规则定死:数到雷就填数字、就地收边,只有数到 0 的空格才继续蔓延。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 扫雷游戏 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。