529. Minesweeper
Problem Description
The Minesweeper game consists of a board with mines ('M') and empty squares ('E'). The objective is to reveal all squares without triggering a mine. When a player clicks a square:
- If a mine is revealed ('M'), the game ends and the mine becomes an 'X'.
- If an empty square ('E') without adjacent mines is revealed, it becomes a 'B', representing a blank space, and its adjacent unrevealed squares should also be revealed recursively.
- If an empty square ('E') with adjacent mines is revealed, it shows a digit ('1' to '8') representing the number of mines in the adjacent squares.
- The board state is returned when no more squares can be revealed.
The task is to implement the logic that simulates this part of Minesweeper game behavior.
Flowchart Walkthrough
To solve the problem presented in Leetcode 529, Minesweeper, let's use the Flowchart to deduce the appropriate algorithm pattern. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The Minesweeper board can be treated as a graph where each cell is a node with adjacency to its neighboring cells (up to 8 in a typical grid configuration).
Is it a tree?
- No: The board represents a grid, not inherently hierarchical and can contain cycles, e.g., one can traverse in a loop from cell to neighboring cells.
Is the problem related to directed acyclic graphs (DAGs)?
- No: This problem involves exploring a grid to reveal cells, not processing directed acyclic data structures.
Is the problem related to shortest paths?
- No: The objective is to reveal cells based on mines' locations rather than finding a path between two points.
Does the problem involve connectivity?
- Yes: The core of Minesweeper's gameplay is to reveal parts of the grid that are connected (i.e., open an area of non-mined cells).
Is the graph weighted?
- No: Connections in the Minesweeper board (edges in the graph, conceptually) do not have weights. They merely express adjacency and the absence of a direct obstacle (mine).
Conclusion: The flowchart suggests using BFS or DFS approaches for this unweighted connectivity problem in Minesweeper. Depth-First Search (DFS) is particularly well-suited for exploring and opening connected cells on the Minesweeper grid since it will naturally follow through the open cells until it reaches cells that require user action (cells near mines), making it efficient for this type of exploration.
Intuition
To solve this problem, we use Depth-First Search (DFS) algorithm, where we start from the clicked position and explore adjacent squares. Here's a step-by-step breakdown:
-
Check the square at the clicked position:
- If it's a mine ('M'), change it to an 'X'.
- If it's an empty square ('E'), perform the following:
- Count the number of mines in the adjacent squares.
- If there are adjacent mines, update the square with the mine count.
- If there are no adjacent mines, update the square to a 'B' and recursively reveal adjacent 'E' squares.
-
The base case for recursive DFS is when the current square is not an 'E' or when it is out of bounds, in which case we just return.
-
By following the steps above, we ensure that we reveal only the necessary squares to either show the number of adjacent mines or to expand the empty regions ('E' to 'B').
-
The recursion naturally takes care of revealing all connected empty squares ('E' to 'B') and stopping when it encounters numbered squares or the edge of the board.
This DFS approach encapsulates the essence of Minesweeper's revealing mechanic, carefully unveiling the board according to the game's rules.
Learn more about Depth-First Search and Breadth-First Search patterns.
Solution Approach
The implementation of the solution is a straightforward application of Depth-First Search (DFS). Here are the details of the implementation steps, corresponding to the given Python code:
-
The nested
dfs
function is defined to perform a DFS starting from a particular cell on the board. -
The main logic for the DFS method is as follows:
- First, we count the number of mines (
'M'
) around the current square(i, j)
. This is done in a nested loop where we iterate over all adjacent cells including diagonals. - If there is at least one mine adjacent to
(i, j)
, we update the current square with the number of mines (cnt
). - If there are no adjacent mines, we update the current square to
'B'
. We then recursively calldfs
on all adjacent unrevealed squares ('E'
) to continue revealing the board.
- First, we count the number of mines (
-
Before DFS is initiated, we first check the cell at the click coordinates. If the clicked cell is a mine (
'M'
), we update it to'X'
to indicate the game is over. Otherwise, we call ourdfs
function with the click coordinates. -
The
dfs
function is designed to stop the recursive calls when it encounters a cell that is not'E'
, which handles both the case when revealing'B'
squares, stopping at the border, and stopping at numbered squares. -
The
dfs
function also ensures that we never go out of bounds of the board by checking whether the coordinates(x, y)
are within the range[0, m)
for rows and[0, n)
for columns, wherem
andn
are the lengths of the board's rows and columns respectively. -
The
updateBoard
function then returns the updated board once all the possible and necessary cells are revealed based on the initial click.
This implementation ensures that all recursive calls to reveal cells only act on valid, in-bound, and appropriate cells, preventing unnecessary work and ensuring that the board state is mutated correctly according to Minesweeper rules.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's go through a small example to illustrate the solution approach. Suppose we have a Minesweeper board like this, and the player clicks on the cell at row 1, column 2 (indexes are zero-based):
[['E', 'E', 'E', 'E'], ['E', 'E', 'E', 'M'], ['E', 'M', 'E', 'E'], ['E', 'E', 'M', 'E']]
We will follow the solution steps for our DFS approach using this board:
-
Initialize – We note the player's click on cell (1, 2), which is 'E'.
-
DFS Function Call – We check whether the clicked cell is 'M' or 'E'. Since it's 'E', we invoke our
dfs
function here. -
Count Mines – We look at all the adjacent cells around (1, 2). The adjacent cells contain one mine at (2, 1). Therefore, the mine count
cnt
is 1. -
Update Cell – Since there's one mine adjacent to (1, 2), we update the board as follows, replacing 'E' with '1' to indicate the number of adjacent mines:
[['E', 'E', 'E', 'E'], ['E', 'E', '1', 'M'], ['E', 'M', 'E', 'E'], ['E', 'E', 'M', 'E']]
-
Adjacent Squares – Since cell (1, 2) is next to only one mine, we don't perform recursive calls on its neighbors as per standard Minesweeper rules.
-
Final Board – The
updateBoard
function returns the board since we cannot reveal any further cells based on the click position.
This updated board represents the state after the single click, adhering to the rules of Minesweeper where numbered cells should not reveal their neighbors. If the initial click was on a different 'E' cell, especially one not adjacent to any mines, the recursive dfs
would take effect, revealing a larger area by turning 'E' cells to 'B' and uncovering all interconnected blank spaces.
Solution Implementation
1from typing import List
2
3class Solution:
4 def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
5 # The recursive function to reveal the board starting from the clicked cell.
6 def reveal(i: int, j: int):
7 # Count mines around the current cell
8 mine_count = 0
9 for x in range(max(i - 1, 0), min(i + 2, rows)): # Limits the range on the board
10 for y in range(max(j - 1, 0), min(j + 2, columns)): # Limits the range on the board
11 if board[x][y] == "M":
12 mine_count += 1
13
14 # If there are mines around the cell, update with mine count
15 if mine_count > 0:
16 board[i][j] = str(mine_count)
17 else:
18 # Otherwise, set the cell to "B" for blank and reveal surrounding cells
19 board[i][j] = "B"
20 for x in range(max(i - 1, 0), min(i + 2, rows)):
21 for y in range(max(j - 1, 0), min(j + 2, columns)):
22 if board[x][y] == "E":
23 reveal(x, y)
24
25 # Get the size of the board
26 rows, columns = len(board), len(board[0])
27
28 # The clicked position
29 click_row, click_col = click
30
31 # If the clicked cell contains a mine, game over
32 if board[click_row][click_col] == "M":
33 board[click_row][click_col] = "X"
34 else:
35 # Start revealing from the clicked cell
36 reveal(click_row, click_col)
37
38 # Return the updated board
39 return board
40
1class Solution {
2 private char[][] gameBoard; // Renamed board to gameBoard for clarity
3 private int rows; // Renamed m to rows for clarity
4 private int cols; // Renamed n to cols for clarity
5
6 // Function updates the game board when a user clicks on a cell (i.e., uncovers the cell)
7 public char[][] updateBoard(char[][] board, int[] click) {
8 rows = board.length; // Total number of rows in the board
9 cols = board[0].length; // Total number of columns in the board
10 gameBoard = board; // Assign board to gameBoard for internal use
11
12 int clickRow = click[0], clickCol = click[1]; // Row and column indexes of the click position
13
14 // Check if the clicked cell contains a mine
15 if (gameBoard[clickRow][clickCol] == 'M') {
16 gameBoard[clickRow][clickCol] = 'X'; // If there's a mine, mark the cell as 'X'
17 } else {
18 // If the clicked cell does not contain a mine, perform Depth First Search (DFS) from this cell
19 dfs(clickRow, clickCol);
20 }
21 return gameBoard; // Return the updated board
22 }
23
24 // Performs Depth First Search (DFS) to reveal cells
25 private void dfs(int row, int col) {
26 int mineCount = 0; // Counter for adjacent mines
27
28 // Iterate through the adjacent cells
29 for (int x = row - 1; x <= row + 1; ++x) {
30 for (int y = col - 1; y <= col + 1; ++y) {
31 // Check if the adjacent cell is within the board and if it contains a mine
32 if (x >= 0 && x < rows && y >= 0 && y < cols && gameBoard[x][y] == 'M') {
33 mineCount++; // Increment the mine counter
34 }
35 }
36 }
37
38 if (mineCount > 0) {
39 // If there are mines found around the cell, display the mine count
40 gameBoard[row][col] = (char) (mineCount + '0');
41 } else {
42 // If there are no mines around the cell, mark the cell as 'B' for blank
43 gameBoard[row][col] = 'B';
44 // Recursively reveal adjacent cells that are not mines and are unrevealed ('E')
45 for (int x = row - 1; x <= row + 1; ++x) {
46 for (int y = col - 1; y <= col + 1; ++y) {
47 if (x >= 0 && x < rows && y >= 0 && y < cols && gameBoard[x][y] == 'E') {
48 dfs(x, y); // Recursive call to dfs
49 }
50 }
51 }
52 }
53 }
54}
55
1class Solution {
2public:
3 vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
4 int rows = board.size(), cols = board[0].size();
5 int clickRow = click[0], clickCol = click[1];
6
7 // Using lambda function to perform Depth-First Search (DFS)
8 function<void(int, int)> dfs = [&](int row, int col) {
9 int mineCount = 0;
10
11 // Count mines in adjacent cells
12 for (int x = row - 1; x <= row + 1; ++x) {
13 for (int y = col - 1; y <= col + 1; ++y) {
14 if (x >= 0 && x < rows && y >= 0 && y < cols && board[x][y] == 'M') {
15 ++mineCount;
16 }
17 }
18 }
19
20 // If mines exist around the cell, update the cell with the mine count. Otherwise, mark the cell as 'B' and continue DFS.
21 if (mineCount > 0) {
22 board[row][col] = mineCount + '0';
23 } else {
24 board[row][col] = 'B';
25 for (int x = row - 1; x <= row + 1; ++x) {
26 for (int y = col - 1; y <= col + 1; ++y) {
27 if (x >= 0 && x < rows && y >= 0 && y < cols && board[x][y] == 'E') {
28 dfs(x, y);
29 }
30 }
31 }
32 }
33 };
34
35 // Handle the initial click
36 if (board[clickRow][clickCol] == 'M') {
37 // If the clicked cell contains a mine, game over
38 board[clickRow][clickCol] = 'X';
39 } else {
40 // If the clicked cell is empty ('E'), perform DFS to reveal cells
41 dfs(clickRow, clickCol);
42 }
43
44 return board;
45 }
46};
47
1function updateBoard(board: string[][], click: number[]): string[][] {
2 const rowCount = board.length; // Number of rows in the board
3 const colCount = board[0].length; // Number of columns in the board
4 const [rowClicked, colClicked] = click; // Destructure click into row and column indices
5
6 // Deep-First Search function to reveal the board
7 const revealBoard = (row: number, col: number) => {
8 let mineCount = 0;
9 // Count the number of mines surrounding the current cell
10 for (let x = row - 1; x <= row + 1; ++x) {
11 for (let y = col - 1; y <= col + 1; ++y) {
12 if (x >= 0 && x < rowCount && y >= 0 && y < colCount && board[x][y] === 'M') {
13 mineCount++;
14 }
15 }
16 }
17
18 if (mineCount > 0) { // If mines are found around the cell, update the cell with the mine count
19 board[row][col] = mineCount.toString();
20 } else { // If no mines are found, mark the cell as 'B' and continue the search
21 board[row][col] = 'B';
22 for (let x = row - 1; x <= row + 1; ++x) {
23 for (let y = col - 1; y <= col + 1; ++y) {
24 // Recursively reveal the board for surrounding cells
25 if (x >= 0 && x < rowCount && y >= 0 && y < colCount && board[x][y] === 'E') {
26 revealBoard(x, y);
27 }
28 }
29 }
30 }
31 };
32
33 // Click processing
34 if (board[rowClicked][colClicked] === 'M') {
35 // If the click is on a mine, game is over. Mark the clicked mine with 'X'
36 board[rowClicked][colClicked] = 'X';
37 } else {
38 // If the click is not on a mine, reveal the board from the clicked spot
39 revealBoard(rowClicked, colClicked);
40 }
41
42 return board; // Return the updated board state
43}
44
Time and Space Complexity
The given Python code is a solution for the Minesweeper game where a player clicks on a cell on the board and depending on the board cell content, different actions may be taken.
Time Complexity:
The time complexity is determined by the number of times dfs
is invoked and how many cells it inspects each time. In the worst-case scenario, dfs
performs a Depth-First Search on the entire board starting from the clicked cell.
- Each cell is visited at most once due to the fact that once a cell is visited it changes from
"E"
(empty) to"B"
(blank) or it becomes a digit representing the number of adjacent mines. - For each cell visited,
dfs
inspects up to 8 adjacent cells.
Given a board of size m x n
, the time complexity is thus O(m * n)
since in the worst-case scenario, we might end up visiting each cell once.
Space Complexity:
The space complexity of this DFS solution is primarily determined by the recursion stack used by the dfs
function.
- In the worst-case scenario, the DFS could recurse to a depth where the entire board is traversed, making the maximum recursion depth
m * n
in a situation where all cells are empty ("E") and connected, requiring the algorithm to visit every cell before hitting a base case.
The space complexity is hence O(m * n)
due to the recursion stack in the worst-case scenario.
When looking at auxiliary space (excluding space for inputs and outputs), the space complexity depends on the number of recursive calls placed on the stack, which remains O(m * n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
How does quick sort divide the problem into subproblems?
Recommended Readings
https algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com 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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!