343 lines
13 KiB
YAML
343 lines
13 KiB
YAML
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
|
||
|
||
|
||
|
||
**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.
|