5
\$\begingroup\$

This is a Leetcode problem -

Let's play the Minesweeper game (Wikipedia, online game)!

You are given a 2D char matrix representing the game board. 'M' represents an unrevealed mine, 'E' represents an unrevealed empty square, 'B' represents a revealed blank square that has no adjacent (above, below, left, right, and all 4 diagonals) mines, digit (1 to 8) represents how many mines are adjacent to this revealed square, and finally 'X' represents a revealed mine.

Now given the next click position (row and column indices) among all the unrevealed squares (M or E), return the board after revealing this position according to the following rules -

  • If a mine (M) is revealed, then the game is over - change it to 'X'.
  • If an empty square (E) with no adjacent mines is revealed, then change it to a revealed blank (B) and all of its adjacent unrevealed squares should be revealed recursively.
  • If an empty square (E) with at least one adjacent mine is revealed, then change it to a digit (1 to 8) representing the number of adjacent mines.
  • Return the board when no more squares will be revealed.

Example 1 -

Input: 

[['E', 'E', 'E', 'E', 'E'],
 ['E', 'E', 'M', 'E', 'E'],
 ['E', 'E', 'E', 'E', 'E'],
 ['E', 'E', 'E', 'E', 'E']]

Click : [3,0]

Output: 

[['B', '1', 'E', '1', 'B'],
 ['B', '1', 'M', '1', 'B'],
 ['B', '1', '1', '1', 'B'],
 ['B', 'B', 'B', 'B', 'B']]

Explanation -

enter image description here

Example 2 -

Input: 

[['B', '1', 'E', '1', 'B'],
 ['B', '1', 'M', '1', 'B'],
 ['B', '1', '1', '1', 'B'],
 ['B', 'B', 'B', 'B', 'B']]

Click : [1,2]

Output: 

[['B', '1', 'E', '1', 'B'],
 ['B', '1', 'X', '1', 'B'],
 ['B', '1', '1', '1', 'B'],
 ['B', 'B', 'B', 'B', 'B']]

Explanation -

enter image description here

Note -

  • The range of the input matrix's height and width is [1,50].
  • The click position will only be an unrevealed square (M or E), which also means the input board contains at least one clickable square.
  • The input board won't be a stage when the game is over (some mines have been revealed).
  • For simplicity, not mentioned rules should be ignored in this problem. For example, you don't need to reveal all the unrevealed mines when the game is over, consider any cases that you will win the game or flag any squares.

Here is my solution to this challenge -

class Solution:

    directions = [(-1, 0), (-1, -1), (-1, 1), (0,-1), (0, 1), (1, -1), (1, 0), (1, 1)]

    def updateBoard(self, board, click):
        """
        :type board: List[List[str]]
        :type click: List[int]
        :rtype: List[List[str]]
        """
        return self.dfs(board, click)

    def dfs(self, board, click):
        stack = [(click[0], click[1])]
        m, n = len(board), len(board[0])
        while stack:
            r, c = stack.pop() # last inserted element

            if board[r][c] == 'M':
                board[r][c] = 'X'
                break

            # check for adjacent mines
            mines = 0
            for i, j in self.directions:
                dr = r + i
                dc = c + j
                if 0 <= dr < m and 0 <= dc < n and board[dr][dc] == 'M':
                    mines += 1
            board[r][c] = str(mines) if mines else 'B'

            # add neighbors
            for i, j in self.directions:
                dr = r + i
                dc = c + j
                if 0 <= dr < m and 0 <= dc < n and board[r][c] == 'B' and board[dr][dc] == 'E':
                    stack.append((dr, dc))

        return board

Here is my Leetcode result (54 test cases) -

enter image description here

So, I would like to know whether I could make this program shorter and more efficient.

Any help would be highly appreciated.

\$\endgroup\$
4
\$\begingroup\$

Instead of repeating 0 <= dr < m and 0 <= dc < n tests in the mine counting loop and the recursive search step, looping over the 2x2, 2x3, 3x2 or 3x3 grid for any r,c coordinate might be better done using a generator expression, using ranges:

while stack:
    r, c = stack.pop()

    rows = range(max(0, r-1), min(r+2, m))
    cols = range(max(0, c-1), min(c+2, n))

    mines = sum(1 for i in rows for j in cols if board[i][j] == 'M')

As a bonus, you get to reuse those ranges if you find a blank square, for the queuing of virtual click coordinates.

    if mines:
        board[r][c] = str(mines)
    else:
        board[r][c] = 'B'
        for i in rows:
            for j in cols:
                if board[i][j] == 'E':
                    stack.append((i, j))

When you click on a square, and find it is empty, and there are no mines around it, you mark it B, and immediately "click" every E square around it (a, b, c, d, f, g, h, & i):

. . . . . . . .
. . . . . . . .
. . a b c . . .
. . d B f . . .
. . g h i . . .
. . . . . . . .
. . . . . . . .

This adds positions a, b, c, d, f, g, h, & i to stack. You pop position i off the stack, and if it turns out to also be surrounded by 0 mines, you add its E neighbours to stack, including f and h a second time. Eventually, you'll pop h off the stack, and add if it is surrounded by zero mines, add d & g a second time, and f a third time. This leads to needlessly counting up the mines around various positions multiple times during one dfs call. Needless repeated counting will, of course, waste time and slow your algorithm down.

Instead of using a list as a stack, you could use a set, which would ignore any duplicate coordinates which are attempted to be queued for subsequent processing.

Alternately, you could mark the "just queued" position with a different symbol, say _, which will eventually be filled in with the number of mines surrounding that square (or B) once that position is popped off the stack for processing. However, it will no longer be an E, so it won't be accidentally queued multiple times.

    else:
        for i in rows:
            for j in cols:
                if board[i][j] == 'E':
                    stack.append((i, j))
                    board[i][j] = '_'    # Temporary mark to prevent repeated queueing

Using slices for counting the number of mines may be slightly faster, due to the fewer indexing operations:

while stack:
    r, c = stack.pop()

    rmin, rmax = max(0, r-1), min(r+2, m)
    cmin, cmax = max(0, c-1), min(c+2, n)

    mines = sum(cell == 'M' for row in board[rmin:rmax] for cell in row[cmin:cmax])
    if mines:
        board[r][c] = str(mines)
    else:
        board[r][c] = 'B'
        for i in range(rmin, rmax):
            for j in range(cmin, cmax):
                if board[i][j] == 'E':
                    stack.append((i, j))
                    board[i][j] = '_'    # Temporary mark to prevent repeated queueing
\$\endgroup\$
4
\$\begingroup\$
  • An M field is never pushed. It may only happen as a result of an unfortunate click. Testing for board[r][c] == 'M' in the loop is a waste of time. Test it once, if board[click[0]][click[1]] == 'M', before the loop.

  • Similarly, you should not bother with neighbors if the field is not blank. A test for board[r][c] == 'B' happens too late. As implemented, for other fields code still compute neighbors - and does nothing with them.

  • The tests for 0 <= dr < m and 0 <= dc < n may also cause performance issues, and do not look Pythonic. Ask forgiveness not permission, e.g.

        try:
            if board[dr][dc] == 'M':
                mines += 1
        except IndexError:
            pass
    
  • Quite a few fields are pushed and inspected multiple times. I am not sure wether it is a bottleneck; however the right-hand and scanline methods of flood fill algorithm do worth investigation.

\$\endgroup\$
  • 1
    \$\begingroup\$ Unfortunately, board[-1][-1] is not a IndexError so that optimization is actually a bug. But if you ask, I’ll forgive you. \$\endgroup\$ – AJNeufeld Jun 4 at 4:00
  • 1
    \$\begingroup\$ @AJNeufeld Forgiveness accepted. \$\endgroup\$ – vnp Jun 4 at 4:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.