在问 · 算法题解

529. 扫雷游戏

中等图论广度优先搜索力扣原题 ↗

题目描述

输入 m x n 的字符矩阵 board 表示扫雷盘面。'M' 是还没挖开的雷,'E' 是还没挖开的空格,'B' 是已挖开且周围 8 格都没有雷的空格,'1' 到 '8' 是已挖开的格子并表示周围 8 格中雷的个数,'X' 是已挖开的雷。

输入 click = [r, c],表示要挖开第 r 行第 c 列(下标从 0 开始)的格子,该格一定是 'M' 或 'E'。

挖到 'M' 时,把它改成 'X',游戏结束。挖到 'E' 时,先数它周围 8 格里的雷:若有雷,就把它改成对应的数字字符;若一个都没有,就改成 'B',并继续对周围 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"]]

解释:点到左下角空白处,连片展开空白区域,雷周围的格子显示数字 1,雷本身和被数字隔开的 (0,2) 保持未挖开。

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

解释:点到雷 (1,2),把它改成 'X',其余格子不变。

数据范围

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 50
  • board[i][j] 是 'M'、'E'、'B' 或 '1' 到 '8' 中的一个。
  • click.length == 2
  • 0 <= click[0] < m
  • 0 <= click[1] < n
  • board[click[0]][click[1]] 是 'M' 或 'E'。

解题思路:广度优先搜索

这是从点击格出发的 BFS。每出队一个格子,先数它周围 8 格的雷:有雷就写数字并停下(数字格是揭开区域的边界);没有雷就写 B,再把周围仍是 E 且没入过队的格子入队。只有 B 格才会继续扩散,所以揭开的区域被一圈数字包住。

为什么正确

队列里的格子都是待揭开的 E 格;seen 保证每个格子最多入队一次,因此每格只被处理一次。

复杂度分析

时间 O(mn),每格最多处理一次、每次查看 8 个邻居;空间 O(mn)。

易错点

只有周围没有雷的格子才能扩展。如果数字格也把邻居入队,就会把不该揭开的格子揭开;另外要在入队时标记,避免同一格重复入队。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
        r, c = click
        if board[r][c] == 'M':
            board[r][c] = 'X'
            return board
        m, n = len(board), len(board[0])
        dirs = [(dx, dy) for dx in (-1, 0, 1) for dy in (-1, 0, 1) if dx or dy]
        queue = collections.deque([(r, c)])
        seen = {(r, c)}
        while queue:
            x, y = queue.popleft()
            mines = 0
            for dx, dy in dirs:
                i, j = x + dx, y + dy
                if 0 <= i < m and 0 <= j < n and board[i][j] == 'M':
                    mines += 1
            if mines > 0:
                board[x][y] = str(mines)
                continue
            board[x][y] = 'B'
            for dx, dy in dirs:
                i, j = x + dx, y + dy
                if 0 <= i < m and 0 <= j < n and board[i][j] == 'E' and (i, j) not in seen:
                    seen.add((i, j))
                    queue.append((i, j))
        return board

载入编辑器,自己跑一遍 →

其他解法

BFS 用队列逐圈展开,需要单独的访问标记防止重复入队;DFS 递归展开,直接用格子状态判重,两者复杂度相同。

解法 2:深度优先搜索 时间 O(mn),空间 O(mn)

从点击位置递归。先统计周围 8 格的地雷数,若不为 0,就写上数字并停止。否则标记为 B,再对周围仍为 E 的格子递归展开。修改后的格子不再是 E,所以不会被重复访问,也不需要额外的访问集合。

代码更短,利用棋盘本身的状态判重。递归深度最坏可达 m·n,棋盘很大时有栈溢出风险。

class Solution:
    def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
        r, c = click
        if board[r][c] == 'M':
            board[r][c] = 'X'
            return board
        m, n = len(board), len(board[0])

        def dfs(x, y):
            # 统计周围 8 格的地雷数
            mines = 0
            for i in range(x - 1, x + 2):
                for j in range(y - 1, y + 2):
                    if 0 <= i < m and 0 <= j < n and board[i][j] == 'M':
                        mines += 1
            if mines:
                board[x][y] = str(mines)
                return
            # 先标记为 B 再递归,避免重复访问
            board[x][y] = 'B'
            for i in range(x - 1, x + 2):
                for j in range(y - 1, y + 2):
                    if 0 <= i < m and 0 <= j < n and board[i][j] == 'E':
                        dfs(i, j)

        dfs(r, c)
        return board

图论 专题的其他题目

查看 图论 全部题目 →