LeetCode 130 - Surrounded Regions
The problem gives us a two dimensional grid called board, where each cell contains either 'X' or 'O'. The goal is to modify the board in place by capturing every region of 'O' cells that is completely surrounded by 'X'.
Difficulty: 🟡 Medium
Topics: Array, Depth-First Search, Breadth-First Search, Union-Find, Matrix
Solution
Problem Understanding
The problem gives us a two dimensional grid called board, where each cell contains either 'X' or 'O'. The goal is to modify the board in place by capturing every region of 'O' cells that is completely surrounded by 'X'.
A region is formed by connecting adjacent 'O' cells horizontally or vertically. Diagonal connections do not count. A region is considered surrounded if none of its cells touches the border of the board. If even one cell in the region lies on the edge, or is connected to an edge 'O', then the entire region must remain unchanged.
The important observation is that surrounded regions are exactly the 'O' regions that cannot reach the border. Any 'O' connected to a border 'O' is safe and should not be flipped.
The input is an m x n matrix where both dimensions are at most 200. That means the board can contain up to 40,000 cells. Because of this constraint, an algorithm that repeatedly scans the board or launches expensive searches from every cell may become too slow. A linear traversal solution, O(m * n), is the ideal target.
Several edge cases are important:
- A single row or single column board cannot contain surrounded regions because every cell touches the border.
- A board with only
'X'cells should remain unchanged. - A board with only
'O'cells should also remain unchanged because all cells are connected to the border. - Large connected regions require careful traversal to avoid revisiting cells repeatedly.
Approaches
Brute Force Approach
A straightforward idea is to examine every 'O' cell independently. For each unvisited 'O', we can perform a DFS or BFS to determine the full connected region. During traversal, we track whether the region touches the border.
If the region does not touch the border, we flip every cell in that region to 'X'. Otherwise, we leave the region unchanged.
This approach is correct because every region is explored completely before deciding whether it should be captured. However, the naive implementation can become inefficient if searches repeatedly revisit the same cells or if we launch a fresh traversal from many positions unnecessarily.
A poorly optimized brute force solution may approach O((m * n)^2) in the worst case.
Optimal Approach
The key insight is that we do not actually need to identify surrounded regions directly. Instead, we only need to identify the regions that are not surrounded.
Any 'O' connected to the border can never be captured. Therefore:
- Start from all border
'O'cells. - Traverse all connected
'O'cells using DFS or BFS. - Temporarily mark these safe cells.
- After traversal:
- Any remaining
'O'must be surrounded, so flip it to'X'. - Restore the temporarily marked safe cells back to
'O'.
This transforms the problem into a graph reachability problem. Rather than testing every region individually, we only protect the regions that are guaranteed to survive.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((m * n)^2) | O(m * n) | Repeatedly explores regions from many cells |
| Optimal | O(m * n) | O(m * n) | Marks border connected regions once |
Algorithm Walkthrough
- First, check whether the board is empty. If the board has no rows or columns, there is nothing to process.
- Traverse all border cells. These include:
- The first row
- The last row
- The first column
- The last column
- Whenever a border cell contains
'O', launch a DFS or BFS from that cell. During traversal:
-
Mark every reachable
'O'as a temporary value such as'T' -
Continue exploring neighboring cells in four directions:
-
Up
-
Down
-
Left
-
Right
- After all border connected regions are marked, scan the entire board again.
- For each cell:
- If the cell is still
'O', it means it was never connected to the border, so it is surrounded. Convert it to'X'. - If the cell is
'T', restore it back to'O'because it belongs to a safe region.
- The board is now fully updated in place.
Why it works
The algorithm relies on one important invariant:
Any 'O' connected to the border cannot be surrounded.
By starting traversal only from border 'O' cells, we mark exactly the cells that must survive. Every unmarked 'O' therefore belongs to a fully enclosed region and can safely be flipped to 'X'.
Because every cell is processed at most a constant number of times, the algorithm is both correct and efficient.
Python Solution
from typing import List
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
if not board or not board[0]:
return
rows = len(board)
cols = len(board[0])
def dfs(r: int, c: int) -> None:
if (
r < 0 or r >= rows or
c < 0 or c >= cols or
board[r][c] != 'O'
):
return
board[r][c] = 'T'
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
# Process left and right borders
for r in range(rows):
if board[r][0] == 'O':
dfs(r, 0)
if board[r][cols - 1] == 'O':
dfs(r, cols - 1)
# Process top and bottom borders
for c in range(cols):
if board[0][c] == 'O':
dfs(0, c)
if board[rows - 1][c] == 'O':
dfs(rows - 1, c)
# Final conversion
for r in range(rows):
for c in range(cols):
if board[r][c] == 'O':
board[r][c] = 'X'
elif board[r][c] == 'T':
board[r][c] = 'O'
The implementation begins by handling empty input safely. The dimensions of the board are stored in rows and cols for easier access.
The dfs helper function performs depth first traversal. It immediately stops when:
- The position goes out of bounds
- The cell is not
'O'
Whenever a valid 'O' is found, it is temporarily marked as 'T'. This prevents revisiting the same cell and distinguishes safe cells from surrounded ones.
The algorithm then scans all border cells. Every border 'O' launches a DFS traversal that marks all connected safe cells.
After all safe regions are identified, the board is scanned one final time:
- Remaining
'O'cells are surrounded and flipped to'X' - Temporary
'T'cells are restored back to'O'
This satisfies the in place modification requirement because no separate board is created.
Go Solution
func solve(board [][]byte) {
if len(board) == 0 || len(board[0]) == 0 {
return
}
rows := len(board)
cols := len(board[0])
var dfs func(int, int)
dfs = func(r int, c int) {
if r < 0 || r >= rows || c < 0 || c >= cols || board[r][c] != 'O' {
return
}
board[r][c] = 'T'
dfs(r+1, c)
dfs(r-1, c)
dfs(r, c+1)
dfs(r, c-1)
}
// Process left and right borders
for r := 0; r < rows; r++ {
if board[r][0] == 'O' {
dfs(r, 0)
}
if board[r][cols-1] == 'O' {
dfs(r, cols-1)
}
}
// Process top and bottom borders
for c := 0; c < cols; c++ {
if board[0][c] == 'O' {
dfs(0, c)
}
if board[rows-1][c] == 'O' {
dfs(rows-1, c)
}
}
// Final conversion
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if board[r][c] == 'O' {
board[r][c] = 'X'
} else if board[r][c] == 'T' {
board[r][c] = 'O'
}
}
}
}
The Go solution follows exactly the same algorithmic structure as the Python implementation. One difference is that the board uses [][]byte rather than strings for individual characters. This allows direct in place modification of cells.
Go also requires explicit declaration of the recursive function variable before assigning the closure to it. Other than syntax differences, the traversal and marking logic remain identical.
Worked Examples
Example 1
Input:
[
["X","X","X","X"],
["X","O","O","X"],
["X","X","O","X"],
["X","O","X","X"]
]
Initial board:
| Row | State |
|---|---|
| 0 | X X X X |
| 1 | X O O X |
| 2 | X X O X |
| 3 | X O X X |
The algorithm first scans the border.
Border cells containing 'O':
(3, 1)
DFS starts from (3, 1).
Board after marking safe region:
| Row | State |
|---|---|
| 0 | X X X X |
| 1 | X O O X |
| 2 | X X O X |
| 3 | X T X X |
Now the algorithm scans the full board.
All remaining 'O' cells are surrounded:
(1,1)(1,2)(2,2)
These are flipped to 'X'.
Temporary 'T' is restored to 'O'.
Final board:
| Row | State |
|---|---|
| 0 | X X X X |
| 1 | X X X X |
| 2 | X X X X |
| 3 | X O X X |
Example 2
Input:
[
["X"]
]
The board contains only one cell.
Since the cell is already 'X', nothing changes.
Final board:
[
["X"]
]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m * n) | Every cell is visited at most a constant number of times |
| Space | O(m * n) | Recursive DFS stack can grow to the size of the board |
The board traversal itself is linear because each cell transitions through states at most once. The DFS recursion depth may become large if the board contains one massive connected region of 'O' cells, which is why the auxiliary space complexity is also O(m * n) in the worst case.
Test Cases
from typing import List
class Solution:
def solve(self, board: List[List[str]]) -> None:
if not board or not board[0]:
return
rows = len(board)
cols = len(board[0])
def dfs(r: int, c: int) -> None:
if (
r < 0 or r >= rows or
c < 0 or c >= cols or
board[r][c] != 'O'
):
return
board[r][c] = 'T'
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
for r in range(rows):
if board[r][0] == 'O':
dfs(r, 0)
if board[r][cols - 1] == 'O':
dfs(r, cols - 1)
for c in range(cols):
if board[0][c] == 'O':
dfs(0, c)
if board[rows - 1][c] == 'O':
dfs(rows - 1, c)
for r in range(rows):
for c in range(cols):
if board[r][c] == 'O':
board[r][c] = 'X'
elif board[r][c] == 'T':
board[r][c] = 'O'
sol = Solution()
# Example 1
board = [
["X","X","X","X"],
["X","O","O","X"],
["X","X","O","X"],
["X","O","X","X"]
]
sol.solve(board)
assert board == [
["X","X","X","X"],
["X","X","X","X"],
["X","X","X","X"],
["X","O","X","X"]
] # standard surrounded region case
# Example 2
board = [["X"]]
sol.solve(board)
assert board == [["X"]] # single X cell
# Single O cell
board = [["O"]]
sol.solve(board)
assert board == [["O"]] # border O should survive
# All O board
board = [
["O","O"],
["O","O"]
]
sol.solve(board)
assert board == [
["O","O"],
["O","O"]
] # all connected to border
# Fully surrounded center
board = [
["X","X","X"],
["X","O","X"],
["X","X","X"]
]
sol.solve(board)
assert board == [
["X","X","X"],
["X","X","X"],
["X","X","X"]
] # center region captured
# Narrow single row
board = [["O","O","X","O"]]
sol.solve(board)
assert board == [["O","O","X","O"]] # no surrounded regions possible
# Narrow single column
board = [
["O"],
["X"],
["O"]
]
sol.solve(board)
assert board == [
["O"],
["X"],
["O"]
] # border connected vertically
# Complex mixed case
board = [
["X","O","X","X"],
["O","O","X","O"],
["X","O","O","X"],
["X","X","X","X"]
]
sol.solve(board)
assert board == [
["X","O","X","X"],
["O","O","X","O"],
["X","O","O","X"],
["X","X","X","X"]
] # all O cells connected to border
| Test | Why |
|---|---|
| Standard surrounded region | Verifies basic capture behavior |
| Single X cell | Smallest possible board |
| Single O cell | Border connected cell must survive |
| All O board | Ensures border propagation works correctly |
| Fully surrounded center | Validates capture of enclosed region |
| Single row | No interior cells exist |
| Single column | Vertical edge connectivity |
| Complex mixed case | Tests multi direction connectivity |
Edge Cases
Single Row or Single Column
Boards with only one row or one column are important because every cell lies on the border. A naive implementation might incorrectly attempt to capture internal cells even though no true interior exists.
The algorithm handles this naturally because all 'O' cells are discovered during the border traversal phase and marked as safe.
Large Connected Border Region
A board may contain a massive region of 'O' cells connected indirectly to the border through a long path. A buggy solution might only preserve border cells themselves instead of preserving the entire connected component.
The DFS traversal correctly propagates through all connected 'O' cells, ensuring the whole region survives.
Completely Enclosed Island
A region entirely surrounded by 'X' cells is the main capture case. The danger here is accidentally preserving cells because of incomplete traversal or incorrect neighbor checks.
Since the algorithm only preserves border reachable cells, enclosed regions are never marked safe. They remain 'O' during the marking phase and are later flipped to 'X' during the final scan.