title: Surrounded Regions
slug: surrounded-regions
difficulty: medium
leetcode_id: 130
leetcode_url: https://leetcode.com/problems/surrounded-regions/
categories:
  - graphs
  - arrays
patterns:
  - dfs
  - bfs
  - matrix-traversal

function_signature: "def solve(board: list[list[str]]) -> None:"

test_cases:
  visible:
    - input:
        board:
          - ["X", "X", "X", "X"]
          - ["X", "O", "O", "X"]
          - ["X", "X", "O", "X"]
          - ["X", "O", "X", "X"]
      expected:
        - ["X", "X", "X", "X"]
        - ["X", "X", "X", "X"]
        - ["X", "X", "X", "X"]
        - ["X", "O", "X", "X"]
    - input:
        board:
          - ["X"]
      expected:
        - ["X"]
  hidden:
    - input:
        board:
          - ["O", "O", "O"]
          - ["O", "O", "O"]
          - ["O", "O", "O"]
      expected:
        - ["O", "O", "O"]
        - ["O", "O", "O"]
        - ["O", "O", "O"]
    - input:
        board:
          - ["X", "X", "X"]
          - ["X", "O", "X"]
          - ["X", "X", "X"]
      expected:
        - ["X", "X", "X"]
        - ["X", "X", "X"]
        - ["X", "X", "X"]
    - input:
        board:
          - ["O", "X", "X", "O", "X"]
          - ["X", "O", "O", "X", "O"]
          - ["X", "O", "X", "O", "X"]
          - ["O", "X", "O", "O", "O"]
          - ["X", "X", "O", "X", "O"]
      expected:
        - ["O", "X", "X", "O", "X"]
        - ["X", "X", "X", "X", "O"]
        - ["X", "X", "X", "O", "X"]
        - ["O", "X", "O", "O", "O"]
        - ["X", "X", "O", "X", "O"]

description: |
  You are given an `m x n` matrix `board` containing letters `'X'` and `'O'`. **Capture** all regions that are **surrounded**:

  - **Connect**: A cell is connected to adjacent cells horizontally or vertically.
  - **Region**: To form a region, connect every `'O'` cell.
  - **Surround**: A region is surrounded with `'X'` cells if you can connect the region with `'X'` cells and **none** of the region cells are on the edge of the board.

  To capture a surrounded region, replace all `'O'`s with `'X'`s **in-place** within the original board. You do not need to return anything.

constraints: |
  - `m == board.length`
  - `n == board[i].length`
  - `1 <= m, n <= 200`
  - `board[i][j]` is `'X'` or `'O'`

examples:
  - 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 O's in the center form a surrounded region and are captured. The O at the bottom-left edge is not surrounded (it touches the boundary), so it remains unchanged."
  - input: 'board = [["X"]]'
    output: '[["X"]]'
    explanation: "A single X cell has no O's to capture."

explanation:
  intuition: |
    Imagine the board as an island map where `'O'` cells are land and `'X'` cells are water. A region of `'O'`s is "surrounded" if it has no connection to the boundary of the map — it's completely enclosed by water.

    The **key insight** is to think about this problem *backwards*: instead of finding regions that ARE surrounded, find regions that are NOT surrounded (those connected to the boundary), and protect them. Everything else gets captured.

    Think of it like this: any `'O'` on the edge of the board is automatically safe — it can never be fully surrounded. Furthermore, any `'O'` connected to an edge `'O'` is also safe, because the whole connected component touches the boundary.

    So the strategy becomes:
    1. Start from all `'O'`s on the boundary
    2. Mark all `'O'`s connected to them as "safe"
    3. Everything NOT marked safe is surrounded and should be captured

    This "reverse thinking" transforms a complex region-finding problem into a simpler boundary-flood problem.

  approach: |
    We solve this using a **Boundary DFS/BFS Approach**:

    **Step 1: Identify boundary O's**

    - Iterate through all cells on the four edges of the board (first row, last row, first column, last column)
    - When you find an `'O'` on the boundary, it cannot be captured

    &nbsp;

    **Step 2: Mark safe regions**

    - From each boundary `'O'`, perform DFS (or BFS) to visit all connected `'O'` cells
    - Mark these cells with a temporary marker (e.g., `'T'` for "temporary" or "safe")
    - This marks the entire connected component as uncapturable

    &nbsp;

    **Step 3: Capture and restore**

    - Iterate through the entire board
    - Any `'O'` remaining is surrounded — convert it to `'X'`
    - Any `'T'` (temporary marker) is safe — restore it to `'O'`

    &nbsp;

    This three-phase approach ensures we correctly identify which regions touch the boundary and which are truly surrounded.

  common_pitfalls:
    - title: Trying to Find Surrounded Regions Directly
      description: |
        A natural first instinct is to iterate through the board, find each `'O'` region, and check if it's surrounded. This is complex because you need to:
        - Track all cells in a region
        - Check if ANY cell touches the boundary
        - Only then decide to capture or not

        The boundary-first approach is simpler: mark safe cells first, then capture everything else in one pass.
      wrong_approach: "Find each O region and check if surrounded"
      correct_approach: "Mark boundary-connected O's first, then capture the rest"

    - title: Forgetting to Check All Four Boundaries
      description: |
        The board has four edges: top row, bottom row, left column, and right column. Missing any edge means some safe `'O'`s won't be marked, leading to incorrect captures.

        Make sure to iterate through:
        - Row 0 and row `m-1` (top and bottom)
        - Column 0 and column `n-1` (left and right)
      wrong_approach: "Only checking top and left edges"
      correct_approach: "Check all four edges of the board"

    - title: Stack Overflow with Deep Recursion
      description: |
        With a 200x200 board, a region could contain up to 40,000 cells. Naive recursive DFS might cause stack overflow on such large connected components.

        Solutions:
        - Use iterative DFS with an explicit stack
        - Use BFS with a queue
        - Increase recursion limit (not recommended)
      wrong_approach: "Deep recursive DFS on large boards"
      correct_approach: "Iterative DFS or BFS for large inputs"

    - title: Modifying While Searching
      description: |
        If you try to capture `'O'`s to `'X'`s while still searching, you might accidentally disconnect parts of a safe region before fully exploring it.

        The temporary marker (`'T'`) prevents this — it distinguishes "visited safe" cells from both `'O'` (unvisited) and `'X'` (wall/captured).

  key_takeaways:
    - "**Reverse thinking**: Sometimes it's easier to find what you DON'T want and protect it, rather than directly finding what you want"
    - "**Boundary-connected components**: Problems involving 'surrounded' often reduce to finding what's connected to the boundary"
    - "**Temporary markers**: Using a third state (`'T'`) allows clean separation of visited-safe, unvisited, and captured cells"
    - "**Pattern recognition**: This is similar to Number of Islands, but with the twist of boundary connectivity — recognise the DFS/BFS on grids pattern"

  time_complexity: "O(m * n). We visit each cell at most twice: once during the boundary DFS/BFS marking phase, and once during the final capture/restore pass."
  space_complexity: "O(m * n) in the worst case for the recursion stack or BFS queue, if almost all cells are `'O'` and connected. The modification is done in-place, so no additional board copy is needed."

solutions:
  - approach_name: Boundary DFS
    is_optimal: true
    code: |
      def solve(board: list[list[str]]) -> None:
          if not board or not board[0]:
              return

          m, n = len(board), len(board[0])

          def dfs(r: int, c: int) -> None:
              # Out of bounds or not an O — stop
              if r < 0 or r >= m or c < 0 or c >= n or board[r][c] != 'O':
                  return

              # Mark as safe (temporary marker)
              board[r][c] = 'T'

              # Explore all four directions
              dfs(r + 1, c)  # down
              dfs(r - 1, c)  # up
              dfs(r, c + 1)  # right
              dfs(r, c - 1)  # left

          # Step 1 & 2: Mark all O's connected to boundary
          for i in range(m):
              dfs(i, 0)      # left edge
              dfs(i, n - 1)  # right edge
          for j in range(n):
              dfs(0, j)      # top edge
              dfs(m - 1, j)  # bottom edge

          # Step 3: Capture surrounded O's, restore safe T's
          for i in range(m):
              for j in range(n):
                  if board[i][j] == 'O':
                      board[i][j] = 'X'  # Capture surrounded
                  elif board[i][j] == 'T':
                      board[i][j] = 'O'  # Restore safe
    explanation: |
      **Time Complexity:** O(m * n) — Each cell is visited at most twice.

      **Space Complexity:** O(m * n) — Recursion stack in worst case.

      We start DFS from every boundary `'O'`, marking connected cells as `'T'` (safe). Then we sweep through the board: remaining `'O'`s are captured, `'T'`s are restored. This cleanly separates boundary-connected regions from surrounded ones.

  - approach_name: Boundary BFS
    is_optimal: true
    code: |
      from collections import deque

      def solve(board: list[list[str]]) -> None:
          if not board or not board[0]:
              return

          m, n = len(board), len(board[0])
          queue = deque()

          # Step 1: Collect all boundary O's
          for i in range(m):
              if board[i][0] == 'O':
                  queue.append((i, 0))
              if board[i][n - 1] == 'O':
                  queue.append((i, n - 1))
          for j in range(n):
              if board[0][j] == 'O':
                  queue.append((0, j))
              if board[m - 1][j] == 'O':
                  queue.append((m - 1, j))

          # Step 2: BFS to mark all safe O's
          while queue:
              r, c = queue.popleft()
              if r < 0 or r >= m or c < 0 or c >= n:
                  continue
              if board[r][c] != 'O':
                  continue

              board[r][c] = 'T'  # Mark as safe

              # Add neighbors to explore
              queue.append((r + 1, c))
              queue.append((r - 1, c))
              queue.append((r, c + 1))
              queue.append((r, c - 1))

          # Step 3: Capture and restore
          for i in range(m):
              for j in range(n):
                  if board[i][j] == 'O':
                      board[i][j] = 'X'  # Capture
                  elif board[i][j] == 'T':
                      board[i][j] = 'O'  # Restore
    explanation: |
      **Time Complexity:** O(m * n) — Each cell processed at most once.

      **Space Complexity:** O(m * n) — Queue size in worst case.

      BFS avoids recursion depth issues. We seed the queue with all boundary `'O'`s, then expand outward marking safe cells. The final pass captures and restores just like the DFS approach. BFS is often preferred for very large grids.

  - approach_name: Union-Find
    is_optimal: false
    code: |
      def solve(board: list[list[str]]) -> None:
          if not board or not board[0]:
              return

          m, n = len(board), len(board[0])

          # Union-Find with path compression
          parent = list(range(m * n + 1))
          rank = [0] * (m * n + 1)
          dummy = m * n  # Virtual node for boundary-connected cells

          def find(x: int) -> int:
              if parent[x] != x:
                  parent[x] = find(parent[x])  # Path compression
              return parent[x]

          def union(x: int, y: int) -> None:
              px, py = find(x), find(y)
              if px == py:
                  return
              # Union by rank
              if rank[px] < rank[py]:
                  px, py = py, px
              parent[py] = px
              if rank[px] == rank[py]:
                  rank[px] += 1

          def index(r: int, c: int) -> int:
              return r * n + c

          # Build unions
          for i in range(m):
              for j in range(n):
                  if board[i][j] != 'O':
                      continue

                  idx = index(i, j)

                  # Connect boundary O's to dummy node
                  if i == 0 or i == m - 1 or j == 0 or j == n - 1:
                      union(idx, dummy)

                  # Connect to adjacent O's
                  if i > 0 and board[i - 1][j] == 'O':
                      union(idx, index(i - 1, j))
                  if j > 0 and board[i][j - 1] == 'O':
                      union(idx, index(i, j - 1))

          # Capture cells not connected to dummy
          for i in range(m):
              for j in range(n):
                  if board[i][j] == 'O' and find(index(i, j)) != find(dummy):
                      board[i][j] = 'X'
    explanation: |
      **Time Complexity:** O(m * n * α(m * n)) — Nearly linear due to path compression.

      **Space Complexity:** O(m * n) — Parent and rank arrays.

      Union-Find groups all `'O'`s into connected components. Boundary `'O'`s are connected to a virtual "dummy" node. After processing, any `'O'` not in the dummy's component is surrounded and captured. This approach is more complex but demonstrates the Union-Find pattern for connectivity problems.
