title: Surrounded Regions slug: surrounded-regions difficulty: medium leetcode_id: 130 leetcode_url: https://leetcode.com/problems/surrounded-regions/ categories: - graphs - arrays patterns: - slug: dfs is_optimal: false - slug: bfs is_optimal: true - slug: matrix-traversal is_optimal: false 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   **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   **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'`   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.