Start learning free

LESSON 505 OF 573 · GRAPHS

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

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 an array of rows, and each row is an array 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 23
    Ctrl + Enter to check

    TYPESCRIPT · GRAPHS

    Surrounded Regions in TypeScript, explained

    Problem overview

    The method solve receives board, a grid of m rows and n columns with single-letter strings 'X' and 'O', stored as an array of rows. Two 'O' cells are connected when one is directly above, below, left or right of the other; diagonal cells are not connected. A region is a group of 'O' cells reachable from each other through such connections.

    A region is surrounded when none of its cells is on the edge of the board, meaning the first or last row or the first or last column. solve captures every surrounded region by turning all its cells into 'X'. A region with a cell on the edge stays as it is. The function changes board in place and returns nothing.

    In the first example, the board

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

    becomes

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

    The three connected 'O' cells in the middle are closed in, so they are captured. The 'O' in the last row is on the edge, so it stays. A board of a single 'O' keeps it, since that cell is on every edge. The algorithm should run in O(m · n) time with O(m · n) extra space.

    Approach: start from the edge instead of from each region

    The direct idea is to look at each 'O' and search for a path of 'O's to the edge. A large region in the middle of the board would be searched again from each of its cells, which can cost O((m · n)^2).

    Turning the question around fixes that. Every 'O' on the edge is safe, and so is every 'O' connected to a safe one. So start one breadth-first search (BFS) from all the edge 'O's at once, and mark every cell it reaches as safe. Each cell joins the queue at most once. After the search, an 'O' that is not marked has no path of 'O's to the edge, so its region is surrounded. One more pass over the board changes those cells to 'X'.

    The search uses a queue that the code manages itself, not recursion. A single region can wind through the whole board. On a 200 × 200 board with a snake-shaped region of 19,701 cells, a recursive depth-first version ran out of call stack with the error "Maximum call stack size exceeded", while the queue version handled it.

    The board in the third example shows the connection rule: in

    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 'O' in the top-left corner touches the region below it only diagonally, so it is a separate region, and it is safe because it is on the edge itself. The middle region winds down to the last row, so it is safe too. The lone 'O' at row 3, column 1 is closed in on all four sides and becomes 'X'.

    Step by step

    1. Read the size. rows is board.length, and cols is board[0].length, since every row has the same length.
    2. Create safe, a boolean[][] of the same size, filled with false. It is built with Array.from({length: rows}, ...) and an arrow function so that each row is a separate array.
    3. Create queue, a number[][] of [row, column] pairs: the safe cells whose neighbors have not been checked yet.
    4. Scan the whole board with two nested loops over r and c.
    5. Decide if the cell is on the edge: onBorder is true for the first or last row, or the first or last column.
    6. Seed the search: if the cell is on the edge and holds 'O', set safe[r][c] to true and push [r, c]. Marking the cell as it joins the queue means no one adds it twice.
    7. Define directions: up, down, left and right as [row change, column change] pairs. Each move changes only one coordinate, so diagonal cells are never neighbors.
    8. Keep head, the index of the next cell to take. Moving an index forward takes cells from the front without shifting the whole array.
    9. Loop while head < queue.length and take the pair at head, destructured into row and col, then advance head.
    10. Visit the four neighbors with for...of over directions, computing nextRow and nextCol.
    11. Check the bounds before using the neighbor as an index; a cell on the edge has neighbors off the board.
    12. Spread: if the neighbor holds 'O' and is not yet safe, mark it and push it.
    13. Capture: after the loop, a second pass turns every 'O' that is not safe into 'X'.

    The solution in TypeScript

    class Solution {
        solve(board: string[][]): void {
            const rows = board.length;
            const cols = board[0].length;
            const safe: boolean[][] = Array.from({length: rows}, () => new Array(cols).fill(false));
            const queue: number[][] = [];
            for (let r = 0; r < rows; r++) {
                for (let c = 0; c < cols; c++) {
                    const onBorder = r === 0 || r === rows - 1 || c === 0 || c === cols - 1;
                    if (onBorder && board[r][c] === 'O') {
                        safe[r][c] = true;
                        queue.push([r, c]);
                    }
                }
            }
            const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
            let head = 0;
            while (head < queue.length) {
                const [row, col] = queue[head];
                head++;
                for (const [dr, dc] of directions) {
                    const nextRow = row + dr;
                    const nextCol = col + dc;
                    if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
                        if (board[nextRow][nextCol] === 'O' && !safe[nextRow][nextCol]) {
                            safe[nextRow][nextCol] = true;
                            queue.push([nextRow, nextCol]);
                        }
                    }
                }
            }
            for (let r = 0; r < rows; r++) {
                for (let c = 0; c < cols; c++) {
                    if (board[r][c] === 'O' && !safe[r][c]) {
                        board[r][c] = 'X';
                    }
                }
            }
        }
    }
    

    TypeScript details:

    • Why Array.from with a function. new Array(3).fill(new Array(3).fill(false)) fills the outer array with the same inner array three times. Setting one cell then shows up in every row: after setting [0][1] the result is [[false,true,false],[false,true,false],[false,true,false]]. The arrow function runs once per row and builds a separate array each time.
    • Destructuring. const [row, col] = queue[head] unpacks a two-element array into two constants, and for (const [dr, dc] of directions) does the same for each move.
    • In-place change. The method is typed void. It writes into the array it received, which the caller sees, and it returns undefined.
    • Strings, not characters. The cells are strings of length one, compared with === against 'O'. TypeScript has no separate character type.
    • Erased types. boolean[][] and number[][] are for the compiler only.

    Complexity

    Time: O(m · n). The seeding pass and the capture pass visit each cell once. In the search each cell joins the queue at most once, because it is marked at that moment, and each one checks four neighbors.

    Space: O(m · n). The safe grid has one entry per cell, and the queue holds up to that many pairs.

    Edge cases and common mistakes

    • A single cell. The board O is unchanged, because the cell is on the edge.
    • Thin boards. With 1 or 2 rows, or 1 or 2 columns, every cell is on the edge, so nothing is captured. A full 3 × 3 board of 'O' keeps all its 'O's, because the middle one is connected to the edge.
    • A lone interior cell. A 3 × 3 board with one 'O' in the middle and 'X' everywhere else turns into all 'X'.
    • Diagonal contact. In a plus-shaped board X O X / O X O / X O X, every 'O' is on the edge and nothing changes. Diagonal contact never connects two cells.
    • A ring. A ring of 'O's closed in by 'X' on all sides is a single region, and the whole ring is captured. In a 5 × 5 board with an 'X' border, a ring around a central 'X' becomes all 'X'.
    • Marking when taking from the queue. Marking a cell only when it leaves the queue lets it be pushed several times by different neighbors. Mark it when it joins.
    • Forgetting the bounds check. A neighbor row of -1 or rows makes board[nextRow] undefined, and reading a cell from it throws "Cannot read properties of undefined". A column out of range does not throw, it just reads undefined, so the bug can stay hidden.
    • Flipping while searching. Changing cells to 'X' during the search loses the information about which cells are connected to the edge. A separate safe grid keeps the two steps apart.
    • Returning a board. The result lives in the board that was passed in; the method returns nothing.

    Testing

    These boards were run through the solution; each result is the board after the call.

    • The four-row example above ends with only the 'O' in the last row.
    • The single-cell board O stays O.
    • The 5 × 5 board with the winding region keeps every 'O' except the lone one at row 3, column 1.
    • A 3 × 3 board of all 'O' stays the same.
    • A 3 × 3 board with 'O' only in the middle becomes all 'X'.
    • The plus-shaped board stays the same.
    • A 5 × 5 board with an 'X' border and a ring of 'O's around one 'X' becomes all 'X'.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free