Facebook Pixel

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:

  1. If you click on a mine ('M'): Game over - change it to 'X' and return the board.

  2. 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.

  3. 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.

  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. First, the game checks if there are any mines adjacent to the clicked square
  2. If there are mines nearby, it shows a number and stops
  3. 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):

  1. 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 to i+1 and j-1 to j+1. For each valid position within bounds that contains a mine ('M'), we increment the counter.

  2. 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')
  3. 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:

  1. In-place Modification: The board is modified directly rather than creating a copy, which is memory efficient.

  2. Boundary Checking: The condition 0 <= x < m and 0 <= y < n ensures we don't access cells outside the board.

  3. 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.

  4. 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 Evaluator

Example 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More