529. Minesweeper
Problem Description
This problem simulates the classic Minesweeper game. You're given a game board represented as an m x n
character matrix where each cell contains one of these characters:
'M'
: An unrevealed mine'E'
: An unrevealed empty square'B'
: A revealed blank square with no adjacent mines'1'
to'8'
: A digit showing the count of adjacent mines for this revealed square'X'
: A revealed mine
You're also given a click position [clickr, clickc]
representing where the player clicks next on an unrevealed square ('M'
or 'E'
).
The task is to update and return the board after this click according to these rules:
-
If you click on a mine (
'M'
): Game over - change it to'X'
and return the board. -
If you click on an empty square (
'E'
) with no adjacent mines: Change it to'B'
and recursively reveal all adjacent unrevealed squares. This creates the "spreading" effect in Minesweeper where clicking on an empty area reveals a large region. -
If you click on an empty square (
'E'
) with at least one adjacent mine: Change it to a digit ('1'
to'8'
) representing the number of adjacent mines. Don't reveal any other squares. -
Stop revealing when no more squares need to be revealed according to the rules above.
Adjacent squares include all 8 directions: horizontally, vertically, and diagonally adjacent cells.
The solution uses depth-first search (DFS) to handle the recursive revealing of squares. When clicking on an empty square, it first counts adjacent mines. If there are adjacent mines, it marks the square with the count. If there are no adjacent mines, it marks the square as 'B'
and recursively reveals all adjacent unrevealed empty squares.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The Minesweeper board can be viewed as a graph where each cell is a node and adjacent cells (including diagonals) are connected by edges. We need to traverse and explore connected components when revealing empty squares.
Is it a tree?
- No: The board structure is not a tree - it's a 2D grid where cells can have multiple paths between them and cycles exist (you can traverse in a square pattern and return to the starting cell).
Is the problem related to directed acyclic graphs?
- No: The Minesweeper board is essentially an undirected graph since adjacency relationships are bidirectional between cells.
Is the problem related to shortest paths?
- No: We're not trying to find the shortest path between cells. Instead, we need to reveal all connected empty squares when clicking on an empty area with no adjacent mines.
Does the problem involve connectivity?
- Yes: The core of the problem involves connectivity - when we click on an empty square with no adjacent mines, we need to reveal all connected empty squares recursively. This is essentially finding and revealing a connected component of empty squares.
Does the problem have small constraints?
- Yes: The board dimensions are typically reasonable (m x n grid), and we're dealing with a single click operation that needs to explore adjacent cells recursively.
DFS/backtracking?
- Yes: DFS is perfect for this recursive exploration pattern where we need to reveal a cell and then recursively reveal all adjacent empty cells if certain conditions are met.
Conclusion: The flowchart suggests using DFS for this problem, which aligns perfectly with the recursive nature of revealing connected empty squares in Minesweeper. DFS allows us to explore each empty cell and recursively process its neighbors when no mines are adjacent.
Intuition
When playing Minesweeper, clicking on an empty area often reveals a large region of the board at once. This "spreading" behavior is the key insight - it's a recursive process where revealing one empty square can trigger the revealing of its neighbors, which in turn reveal their neighbors, and so on.
Think about what happens when you click on an empty square in the actual game:
- First, the game checks if there are any mines adjacent to the clicked square
- If there are mines nearby, it shows a number and stops
- If there are no mines nearby, it reveals the square as blank and automatically reveals all adjacent squares
This recursive revealing pattern naturally maps to a depth-first search approach. Each revealed empty square becomes a starting point for checking its own neighbors, creating a chain reaction of reveals.
The algorithm mirrors exactly what happens in the game:
- When we click on a cell, we first need to check what type it is
- If it's a mine (
'M'
), game over - mark it as'X'
and we're done - If it's an empty cell (
'E'
), we count the adjacent mines:- If there are adjacent mines (count > 0), we mark the cell with the count and stop
- If there are no adjacent mines (count = 0), we mark it as
'B'
and recursively apply the same logic to all adjacent unrevealed cells
The DFS naturally handles the recursive propagation - we explore as deep as possible in one direction (revealing connected empty squares) before backtracking. This ensures we reveal the entire connected region of empty squares with no adjacent mines, just like the cascading reveal effect in the actual Minesweeper game.
The beauty of this approach is that it directly models the game's behavior - we're not trying to optimize or find shortcuts, we're simply implementing the exact rules of how Minesweeper reveals squares, which happens to be a perfect use case for DFS traversal.
Learn more about Depth-First Search and Breadth-First Search patterns.
Solution Approach
The implementation uses DFS to handle the recursive revealing of squares. Let's walk through the key components:
Main Function Structure:
The solution first extracts the board dimensions m
and n
, then handles two cases based on what was clicked:
- If we click on a mine (
board[i][j] == 'M'
), we simply mark it as'X'
and return - Otherwise, we call the DFS function to handle the recursive revealing
DFS Function Implementation:
The dfs(i, j)
function handles revealing an empty square at position (i, j)
:
-
Count Adjacent Mines:
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
We iterate through all 8 adjacent cells (including diagonals) using nested loops from
i-1
toi+1
andj-1
toj+1
. For each valid position within bounds that contains a mine ('M'
), we increment the counter. -
Update Current Cell:
- If
cnt > 0
: Mark the cell with the count as a string (board[i][j] = str(cnt)
) - If
cnt == 0
: Mark the cell as blank (board[i][j] = 'B'
)
- If
-
Recursive Revealing (only if no adjacent mines):
if cnt == 0: 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)
When there are no adjacent mines, we recursively call
dfs
on all adjacent unrevealed empty squares ('E'
). This creates the cascading reveal effect.
Key Design Decisions:
-
In-place Modification: The board is modified directly rather than creating a copy, which is memory efficient.
-
Boundary Checking: The condition
0 <= x < m and 0 <= y < n
ensures we don't access cells outside the board. -
State Tracking: By changing cells from
'E'
to either'B'
or a digit, we naturally prevent infinite recursion - once a cell is revealed, it won't be processed again. -
Two-phase Process: First count all adjacent mines, then decide whether to recurse. This ensures we don't start revealing neighbors before we know if the current cell should show a number.
The algorithm's time complexity is O(m ร n)
in the worst case where we might need to reveal the entire board, and space complexity is O(m ร n)
for the recursion stack in the worst case of revealing all cells.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate how the solution works:
Initial Board:
[['E', 'E', 'E', 'E', 'E'], ['E', 'E', 'M', 'E', 'E'], ['E', 'E', 'E', 'E', 'E'], ['E', 'E', 'E', 'E', 'E']]
Click Position: [3, 0]
(bottom-left corner)
Step 1: Initial Click at [3, 0]
- Check if it's a mine: No, it's 'E'
- Call
dfs(3, 0)
- Count adjacent mines around [3, 0]:
- Check positions [2,0], [2,1], [3,1] (only valid neighbors)
- No mines found, count = 0
- Since count = 0, mark [3, 0] as 'B'
- Recursively reveal all adjacent 'E' squares
Step 2: DFS spreads to [2, 0]
- Count adjacent mines around [2, 0]:
- Check all 5 valid neighbors
- No mines found, count = 0
- Mark [2, 0] as 'B'
- Continue recursive revealing
Step 3: DFS spreads to [1, 0]
- Count adjacent mines around [1, 0]:
- Check all 5 valid neighbors
- Mine found at [1, 2], count = 1
- Mark [1, 0] as '1'
- Stop here (don't recurse since count > 0)
Step 4: DFS continues from [2, 0] to [2, 1]
- Count adjacent mines around [2, 1]:
- Check all 8 neighbors
- Mine found at [1, 2], count = 1
- Mark [2, 1] as '1'
- Stop here
Step 5: Continue revealing connected empty squares The DFS continues this pattern, revealing squares with no adjacent mines as 'B' and squares with adjacent mines showing their count.
Final Board:
[['B', '1', 'E', 'E', 'E'], ['1', '2', 'M', 'E', 'E'], ['B', '1', 'E', 'E', 'E'], ['B', 'B', 'E', 'E', 'E']]
The algorithm reveals:
- All connected empty squares with no adjacent mines as 'B'
- Squares with adjacent mines show their count ('1' or '2')
- Stops at the boundary of numbered squares
- Unrevealed areas remain as 'E'
- The mine remains hidden as 'M' since it wasn't clicked
This demonstrates how DFS creates the cascading reveal effect: starting from the clicked position, it spreads outward revealing all connected empty areas until it hits squares with adjacent mines, which act as boundaries for the reveal.
Solution Implementation
1class Solution:
2 def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
3 """
4 Simulates Minesweeper game board update after a click.
5
6 Args:
7 board: 2D list representing the game board
8 click: [row, col] position of the click
9
10 Returns:
11 Updated board after processing the click
12 """
13
14 def dfs(row: int, col: int) -> None:
15 """
16 Depth-first search to reveal cells recursively.
17
18 Args:
19 row: Current row position
20 col: Current column position
21 """
22 # Count adjacent mines
23 mine_count = 0
24 for x in range(row - 1, row + 2):
25 for y in range(col - 1, col + 2):
26 # Check if position is valid and contains a mine
27 if 0 <= x < rows and 0 <= y < cols and board[x][y] == "M":
28 mine_count += 1
29
30 if mine_count > 0:
31 # If there are adjacent mines, show the count
32 board[row][col] = str(mine_count)
33 else:
34 # No adjacent mines, mark as blank and continue revealing
35 board[row][col] = "B"
36
37 # Recursively reveal all adjacent empty cells
38 for x in range(row - 1, row + 2):
39 for y in range(col - 1, col + 2):
40 # Only process valid positions with unrevealed empty cells
41 if 0 <= x < rows and 0 <= y < cols and board[x][y] == "E":
42 dfs(x, y)
43
44 # Get board dimensions
45 rows, cols = len(board), len(board[0])
46
47 # Extract click coordinates
48 click_row, click_col = click
49
50 # Handle click on mine
51 if board[click_row][click_col] == "M":
52 board[click_row][click_col] = "X" # Game over - mine exploded
53 else:
54 # Click on empty cell - start revealing process
55 dfs(click_row, click_col)
56
57 return board
58
1class Solution {
2 // Instance variables to store board reference and dimensions
3 private char[][] board;
4 private int rows;
5 private int cols;
6
7 /**
8 * Updates the board based on the click position following Minesweeper rules
9 * @param board - 2D character array representing the game board
10 * @param click - array containing row and column indices of the click position
11 * @return updated board after processing the click
12 */
13 public char[][] updateBoard(char[][] board, int[] click) {
14 // Initialize board dimensions
15 this.rows = board.length;
16 this.cols = board[0].length;
17 this.board = board;
18
19 // Extract click coordinates
20 int clickRow = click[0];
21 int clickCol = click[1];
22
23 // If clicked on a mine, reveal it as 'X'
24 if (board[clickRow][clickCol] == 'M') {
25 board[clickRow][clickCol] = 'X';
26 } else {
27 // Otherwise, perform DFS to reveal empty squares
28 revealEmptySquares(clickRow, clickCol);
29 }
30
31 return board;
32 }
33
34 /**
35 * Performs depth-first search to reveal empty squares and count adjacent mines
36 * @param row - current row index
37 * @param col - current column index
38 */
39 private void revealEmptySquares(int row, int col) {
40 // Count adjacent mines
41 int adjacentMineCount = 0;
42
43 // Check all 8 adjacent cells
44 for (int neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
45 for (int neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
46 // Check if neighbor is within bounds and is a mine
47 if (neighborRow >= 0 && neighborRow < rows &&
48 neighborCol >= 0 && neighborCol < cols &&
49 board[neighborRow][neighborCol] == 'M') {
50 adjacentMineCount++;
51 }
52 }
53 }
54
55 // If there are adjacent mines, display the count
56 if (adjacentMineCount > 0) {
57 board[row][col] = (char) (adjacentMineCount + '0');
58 } else {
59 // No adjacent mines - mark as blank and recursively reveal neighbors
60 board[row][col] = 'B';
61
62 // Recursively reveal all adjacent empty cells
63 for (int neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
64 for (int neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
65 // Only process unrevealed empty cells within bounds
66 if (neighborRow >= 0 && neighborRow < rows &&
67 neighborCol >= 0 && neighborCol < cols &&
68 board[neighborRow][neighborCol] == 'E') {
69 revealEmptySquares(neighborRow, neighborCol);
70 }
71 }
72 }
73 }
74 }
75}
76
1class Solution {
2public:
3 vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
4 int rows = board.size();
5 int cols = board[0].size();
6 int clickRow = click[0];
7 int clickCol = click[1];
8
9 // Lambda function to recursively reveal the board using DFS
10 function<void(int, int)> revealBoard = [&](int row, int col) {
11 // Count adjacent mines
12 int mineCount = 0;
13
14 // Check all 8 adjacent cells for mines
15 for (int adjRow = row - 1; adjRow <= row + 1; ++adjRow) {
16 for (int adjCol = col - 1; adjCol <= col + 1; ++adjCol) {
17 // Check if adjacent cell is within bounds and contains a mine
18 if (adjRow >= 0 && adjRow < rows &&
19 adjCol >= 0 && adjCol < cols &&
20 board[adjRow][adjCol] == 'M') {
21 ++mineCount;
22 }
23 }
24 }
25
26 // If there are adjacent mines, display the count
27 if (mineCount > 0) {
28 board[row][col] = mineCount + '0'; // Convert number to character
29 }
30 // If no adjacent mines, mark as blank and recursively reveal neighbors
31 else {
32 board[row][col] = 'B'; // Mark current cell as blank revealed
33
34 // Recursively reveal all adjacent unrevealed empty cells
35 for (int adjRow = row - 1; adjRow <= row + 1; ++adjRow) {
36 for (int adjCol = col - 1; adjCol <= col + 1; ++adjCol) {
37 // Check if adjacent cell is within bounds and unrevealed
38 if (adjRow >= 0 && adjRow < rows &&
39 adjCol >= 0 && adjCol < cols &&
40 board[adjRow][adjCol] == 'E') {
41 revealBoard(adjRow, adjCol);
42 }
43 }
44 }
45 }
46 };
47
48 // Handle the clicked cell
49 if (board[clickRow][clickCol] == 'M') {
50 // If clicked on a mine, game over - mark it as exploded mine
51 board[clickRow][clickCol] = 'X';
52 } else {
53 // If clicked on an empty cell, start revealing process
54 revealBoard(clickRow, clickCol);
55 }
56
57 return board;
58 }
59};
60
1/**
2 * Updates the game board based on a click position (Minesweeper game logic)
3 * @param board - 2D array representing the game board
4 * @param click - Array containing the row and column indices of the click position
5 * @returns Updated game board after processing the click
6 */
7function updateBoard(board: string[][], click: number[]): string[][] {
8 const rowCount = board.length;
9 const colCount = board[0].length;
10 const [clickRow, clickCol] = click;
11
12 /**
13 * Depth-first search to reveal cells recursively
14 * @param row - Current row index
15 * @param col - Current column index
16 */
17 const revealCell = (row: number, col: number): void => {
18 // Count adjacent mines
19 let adjacentMineCount = 0;
20
21 // Check all 8 adjacent cells
22 for (let neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
23 for (let neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
24 // Validate bounds and check if neighbor is a mine
25 if (neighborRow >= 0 && neighborRow < rowCount &&
26 neighborCol >= 0 && neighborCol < colCount &&
27 board[neighborRow][neighborCol] === 'M') {
28 adjacentMineCount++;
29 }
30 }
31 }
32
33 // If there are adjacent mines, display the count and stop recursion
34 if (adjacentMineCount > 0) {
35 board[row][col] = adjacentMineCount.toString();
36 return;
37 }
38
39 // Mark current cell as blank (no adjacent mines)
40 board[row][col] = 'B';
41
42 // Recursively reveal all adjacent empty cells
43 for (let neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
44 for (let neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
45 // Only process valid, unrevealed empty cells
46 if (neighborRow >= 0 && neighborRow < rowCount &&
47 neighborCol >= 0 && neighborCol < colCount &&
48 board[neighborRow][neighborCol] === 'E') {
49 revealCell(neighborRow, neighborCol);
50 }
51 }
52 }
53 };
54
55 // Handle click on mine vs empty cell
56 if (board[clickRow][clickCol] === 'M') {
57 // Mine clicked - game over
58 board[clickRow][clickCol] = 'X';
59 } else {
60 // Empty cell clicked - start revealing process
61 revealCell(clickRow, clickCol);
62 }
63
64 return board;
65}
66
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns in the board.
In the worst case, we need to reveal all cells on the board. This happens when we click on an empty cell with no adjacent mines, and the entire board consists of empty cells except for potential mines at the edges. The DFS will visit each cell at most once since:
- We only process cells marked as
"E"
(unrevealed empty) - Once a cell is processed, it's marked as either
"B"
(blank) or a number"1"
-"8"
, preventing revisiting
For each cell visited, we check up to 8 adjacent cells (3ร3 grid minus the center), which is a constant operation O(1)
. Therefore, the overall time complexity is O(m * n)
.
Space Complexity: O(m * n)
in the worst case.
The space complexity is dominated by the recursion call stack. In the worst-case scenario, when we click on an empty cell with no adjacent mines and need to reveal the entire board, the DFS recursion depth can reach O(m * n)
cells. This happens when the cells form a long chain where each cell leads to exactly one unexplored neighbor, creating a path that visits all cells sequentially.
Since we're modifying the board in-place, no additional space is used for storing the board state beyond the recursion stack.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Adjacent Cell Iteration
A frequent mistake is incorrectly iterating through adjacent cells, either missing the clicked cell itself or using wrong loop boundaries.
Pitfall Example:
# Wrong: This includes (row, col) in the adjacent cells check
for x in range(row - 1, row + 2):
for y in range(col - 1, col + 2):
if board[x][y] == "M": # Counts the current cell if it's a mine
mine_count += 1
Solution: When counting mines, we should exclude the current cell (row, col) from the count since we're looking for adjacent mines only:
for x in range(row - 1, row + 2):
for y in range(col - 1, col + 2):
if (x != row or y != col) and 0 <= x < rows and 0 <= y < cols and board[x][y] == "M":
mine_count += 1
However, in this specific problem, this isn't an issue because the clicked cell is always 'E' when we enter the DFS function (mines are handled separately), so it won't be counted anyway.
2. Modifying Board During Iteration
Another pitfall is attempting to reveal cells while still counting mines, which can lead to incorrect counts or infinite recursion.
Pitfall Example:
def dfs(row, col):
for x in range(row - 1, row + 2):
for y in range(col - 1, col + 2):
if 0 <= x < rows and 0 <= y < cols:
if board[x][y] == "M":
mine_count += 1
elif board[x][y] == "E":
dfs(x, y) # Wrong: Revealing before finishing the count
Solution: Always complete the mine counting phase before making any modifications or recursive calls. The correct approach separates these phases:
# Phase 1: Count all adjacent mines
mine_count = 0
for x in range(row - 1, row + 2):
for y in range(col - 1, col + 2):
if 0 <= x < rows and 0 <= y < cols and board[x][y] == "M":
mine_count += 1
# Phase 2: Update current cell and recurse if needed
if mine_count > 0:
board[row][col] = str(mine_count)
else:
board[row][col] = "B"
# Only now do we recurse
for x in range(row - 1, row + 2):
for y in range(col - 1, col + 2):
if 0 <= x < rows and 0 <= y < cols and board[x][y] == "E":
dfs(x, y)
3. Forgetting to Check Cell State Before Recursion
Failing to verify that an adjacent cell is unrevealed ('E') before making a recursive call can cause unnecessary processing or errors.
Pitfall Example:
if mine_count == 0:
board[row][col] = "B"
for x in range(row - 1, row + 2):
for y in range(col - 1, col + 2):
if 0 <= x < rows and 0 <= y < cols:
dfs(x, y) # Wrong: May recurse on already revealed cells
Solution: Always check that the cell is in the 'E' state before recursing:
if 0 <= x < rows and 0 <= y < cols and board[x][y] == "E": dfs(x, y)
This prevents infinite recursion and unnecessary processing of already-revealed cells.
How does merge sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!