Files
codetutor/backend/data/questions/surrounded-regions.yaml

346 lines
13 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
&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.