Start learning free

LESSON 501 OF 569 · GRAPHS

Surrounded Regions in Python: BFS from the Border in O(m·n)

Practice O(m · n) Read the full explanation

You are given board, a grid of m rows and n columns in which every cell holds the letter 'X' or the letter 'O'. It is a list of rows, and each row is a list of single letters.

Two 'O' cells are connected when one is directly above, below, left or right of the other (not diagonally). A region is a group of 'O' cells that can all be reached from each other through connected cells, and that no other 'O' is connected to.

A region is surrounded when none of its cells lies on the edge of the board (the first or the last row, the first or the last column): 'X' cells close it in on every side. Capture every surrounded region by changing all of its cells to 'X'. A region with at least one cell on the edge is not captured and stays as it is.

Change board in place and return nothing: the board you were given must hold the result.

 

Example 1:

Input: board =
X X X X
X O O X
X X O X
X O X X

Output:
X X X X
X X X X
X X X X
X O X X

Explanation: The three red cells form one region, and none of them is on the edge, so it is
captured. The green 'O' is in the last row, on the edge: it is not captured.

Example 2:

Input: board =
O

Output:
O

Explanation: The only cell is on every edge of the board, so it is not captured.

Example 3:

Input: board =
O X X X X
X O O O X
X X X O X
X O X O X
X X X O X

Output:
O X X X X
X O O O X
X X X O X
X X X O X
X X X O X

Explanation: The green region in the middle winds down to the 'O' in the last row, which is on
the edge, so none of it is captured. The 'O' in the top-left corner touches that region only diagonally,
which doesn't connect them, but it is on the edge itself. The red 'O' is closed in by 'X' on all four
sides: it is captured.

 

Constraints:

  • 1 <= m, n <= 200
  • Every row has n cells.
  • Every cell is 'X' or 'O'.

Note:

Your algorithm should run in O(m · n) time and use O(m · n) extra space.

Checking every 'O' on its own, with a search that looks for a way to the edge, can cost O((m · n)2): think of one large region in the middle of the board that is searched again from each of its cells. The tutorial turns the question around, as Pacific Atlantic Water Flow does, and starts from the edge. Every 'O' on the edge is safe, and so is every 'O' connected to a safe one. A BFS that starts from all the 'O's on the edge at once marks every safe cell, and each cell joins its queue at most once. Then one pass over the board turns every 'O' that is not marked into 'X'.

Use a queue, or a stack of your own, rather than recursion: one region can wind through the whole board, and a recursive search tens of thousands of calls deep runs out of call stack in some languages.

STEP 1 OF 21
    Ctrl + Enter to check

    PYTHON · GRAPHS

    Surrounded Regions in Python, explained

    Problem overview

    The function solve receives board, a list of rows where every cell is the one-letter string 'X' or 'O'. Two 'O' cells are connected when one is directly above, below, left or right of the other; diagonal cells don't count. A region is a group of 'O' cells connected to each other. A region is surrounded when none of its cells lies on the edge of the board, meaning the first or last row or the first or last column. The function turns every cell of every surrounded region into 'X'. Regions that touch the edge stay as they are.

    The function changes board in place and returns nothing. For this board:

    X X X X
    X O O X
    X X O X
    X O X X

    the three 'O' cells at the top form one region, and none is on the edge, so they are captured. The 'O' in the last row is on the edge, so it stays:

    X X X X
    X X X X
    X X X X
    X O X X

    Approach: start from the edge

    The direct approach checks each 'O' and searches for a path of 'O' cells to the edge. A large region in the middle of the board is then searched again from each of its cells, which can cost O((m · n)^2).

    It's cheaper to turn the question around. Every 'O' on the edge is safe, and so is every 'O' connected to a safe one. A breadth-first search that starts from all the edge 'O' cells at once marks every safe cell, and each cell joins the queue at most once. After that, one pass over the board turns every 'O' that was not marked into 'X': it has no path of 'O' cells to the edge, so its region is surrounded.

    The search uses a queue instead of recursion. One region can wind through the whole board, and Python limits the call stack: its default recursion limit is 1000. A recursive depth-first search on a 200 x 200 board whose 'O' cells form one winding path stops with RecursionError: maximum recursion depth exceeded. A queue lives on the heap and has no such limit.

    Take the board with a long region from the edge:

    O X X X X
    X O O O X
    X X X O X
    X O X O X
    X X X O X

    The corner 'O' is on the edge, and so is the 'O' in the last row. From the last row the search climbs through the column and reaches the three 'O' cells of the second row. The cell in the fourth row on the left has only 'X' neighbours and no path to the edge, so it is captured. The result is:

    O X X X X
    X O O O X
    X X X O X
    X X X O X
    X X X O X

    The corner 'O' touches the region only diagonally, but it is on the edge itself, so it stays anyway.

    Step by step

    1. Store the size. rows is len(board) and cols is len(board[0]). Every row has the same length, so the first row gives the width.
    2. Create safe, a grid of booleans the same size as the board, all False. A cell becomes True when it holds an 'O' that is connected to the edge.
    3. Create an empty deque named queue. It holds the safe cells whose neighbours are still to be checked, as (row, column) tuples.
    4. Loop over rows, then columns to find the 'O' cells on the edge.
    5. Compute onBorder: the cell is in the first row, the last row, the first column or the last column, with the four tests joined by or.
    6. Seed the search. If the cell is on the edge and holds 'O', set safe[r][c] to True and append (r, c) to the queue. Marking at the moment of queuing means no cell is queued twice.
    7. Define directions, the four moves up, down, left and right as (row change, column change) tuples.
    8. Run the BFS. While the queue is not empty, take a cell from the front with popleft() and unpack it into row and col.
    9. Visit each neighbour. Compute nextRow and nextCol and check that they are inside the board.
    10. Spread. If the neighbour holds 'O' and is not yet marked, mark it and append it to the queue.
    11. Capture. Go over the board again, and set every cell that holds 'O' and is not marked safe to 'X'.

    The solution in Python

    class Solution:
        def solve(self, board: List[List[str]]) -> None:
            rows = len(board)
            cols = len(board[0])
            safe = [[False] * cols for _ in range(rows)]
            queue = deque()
            for r in range(rows):
                for c in range(cols):
                    onBorder = r == 0 or r == rows - 1 or c == 0 or c == cols - 1
                    if onBorder and board[r][c] == 'O':
                        safe[r][c] = True
                        queue.append((r, c))
            directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
            while queue:
                row, col = queue.popleft()
                for dr, dc in directions:
                    nextRow = row + dr
                    nextCol = col + dc
                    if 0 <= nextRow < rows and 0 <= nextCol < cols:
                        if board[nextRow][nextCol] == 'O' and not safe[nextRow][nextCol]:
                            safe[nextRow][nextCol] = True
                            queue.append((nextRow, nextCol))
            for r in range(rows):
                for c in range(cols):
                    if board[r][c] == 'O' and not safe[r][c]:
                        board[r][c] = 'X'
    

    The Python details worth a closer look:

    • Building the grid. [[False] * cols for _ in range(rows)] creates a new row for each iteration. The shorter [[False] * cols] * rows repeats one row object: after bad[0][0] = True on a 3 x 3 grid built that way, every row reads [True, False, False]. With the comprehension only the first row changes. _ is the usual name for a loop variable that isn't used.
    • Chained comparison. 0 <= nextRow < rows checks both bounds at once. The lower bound matters: a negative index doesn't fail in Python, it counts from the end, so row[-1] is the last cell and a neighbour "above" the first row would silently read the last row.
    • Tuple unpacking. row, col = queue.popleft() and for dr, dc in directions split each tuple into two names.
    • Modifying the argument. board is the caller's list of lists. Assigning board[r][c] = 'X' changes that object, which is how the function reports its result without returning anything. Its return value is None.
    • Truthiness of a deque. An empty deque is false, so while queue: ends when the search has nothing left to visit.

    Complexity

    Time: O(m · n). The first pass visits each cell once. Each cell joins the queue at most once and looks at four neighbours. The final pass visits each cell once.

    Space: O(m · n). The safe grid has one entry per cell, and the queue can hold a large share of the cells at once.

    Edge cases and common mistakes

    • A one-cell board. [['O']] stays O, because the cell is on every edge.
    • A board with no interior. With one or two rows, or one or two columns, every cell is on the edge, so nothing is ever captured.
    • All 'O' on the edge. A 3 x 3 board with 'O' everywhere except the middle stays unchanged, and so does one with 'O' only on the four edge midpoints.
    • An isolated centre. The 3 x 3 board with 'X' everywhere except an 'O' in the middle becomes all 'X'.
    • Diagonal contact. Diagonal cells are not neighbours, so the move list has only four entries, each changing one coordinate.
    • Capturing first. Turning interior 'O' cells into 'X' while searching can cut a path that leads to the edge. The solution only marks during the search and changes the board afterwards.
    • Marking too late. If a cell is marked when it leaves the queue instead of when it enters, the same cell can be queued several times by different neighbours.
    • Recursion on large boards. A recursive search can hit the recursion limit of 1000 calls, as described above.

    Testing

    • The 4 x 4 board above becomes four rows where only the last row keeps an 'O' (in column 1).
    • [['O']] stays [['O']].
    • X X X / X O X / X X X becomes all 'X'.
    • O O O / O X O / O O O stays unchanged.
    • X O X / O X O / X O X stays unchanged: all four 'O' cells are on the edge.
    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free