130. Surrounded Regions
Problem Description
You have a 2D board (matrix) filled with letters 'X' and 'O'. Your task is to find all regions of 'O's that are completely surrounded by 'X's and convert them to 'X's.
A region is formed by connecting 'O' cells that are adjacent to each other horizontally or vertically (not diagonally). A region is considered "surrounded" if all paths from any 'O' in that region to the edge of the board are blocked by 'X's. In other words, if a region of 'O's doesn't touch the boundary of the board, it's surrounded.
The key insight is that any 'O' that is on the edge of the board, or is connected to an 'O' on the edge, cannot be surrounded and should remain as 'O'. All other 'O's should be converted to 'X'.
The solution uses a clever approach:
- Start from all 'O's on the boundary of the board
- Use DFS to mark all 'O's connected to the boundary (these cannot be surrounded) by temporarily changing them to '.'
- After marking all non-surrounded 'O's, traverse the entire board:
- Change all '.' back to 'O' (these were connected to the boundary)
- Change all remaining 'O' to 'X' (these were surrounded)
The modification happens in-place, meaning you modify the original board directly without creating a new one.
For example, if you have a board where some 'O's form an island completely enclosed by 'X's, those 'O's would be converted to 'X'. But if even one 'O' in that group touches the edge, the entire connected group remains as 'O'.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a 2D board where we need to explore connected regions of 'O's. Each cell can be considered a node, and adjacent cells (horizontally or vertically) form edges. We're looking for connected components of 'O's.
Is it a tree?
- No: The structure is not a tree. It's a 2D grid where cells can form cycles (e.g., four 'O's forming a square would create a cycle), and there's no hierarchical parent-child relationship.
Is the problem related to directed acyclic graphs?
- No: The grid connections are undirected (if cell A is adjacent to cell B, then B is also adjacent to A), and cycles can exist.
Is the problem related to shortest paths?
- No: We're not looking for the shortest path between points. We need to identify and mark connected regions.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - finding which 'O's are connected to the boundary and which are completely surrounded.
Does the problem have small constraints?
- Yes: While the board can be reasonably sized (m x n matrix), we can efficiently explore it with DFS since we're only visiting each cell once.
DFS/backtracking?
- Yes: DFS is perfect for exploring connected components. We start from boundary 'O's and recursively explore all connected 'O's to mark them as "safe" from capture.
Conclusion: The flowchart leads us to use DFS (Depth-First Search) for solving this connectivity problem, which aligns perfectly with the solution approach of starting from boundary cells and exploring all connected regions.
Intuition
The key insight comes from thinking about the problem in reverse. Instead of trying to identify which 'O' regions are surrounded, we identify which 'O' regions are NOT surrounded.
Any 'O' that cannot be surrounded must have at least one path to the boundary of the board. This means if we start from the boundary and explore all connected 'O's, we'll find all the 'O's that cannot be captured. Everything else must be surrounded.
Think of it like water flowing from the edges of the board - any 'O' that the water can reach from the boundary cannot be surrounded. The 'O's that remain dry are the ones completely enclosed by 'X's.
This reverse thinking simplifies the problem significantly:
- Instead of checking every 'O' region to see if it's surrounded (which would be complex), we only need to start from the boundary 'O's
- We use DFS to "mark" all 'O's connected to the boundary (temporarily changing them to '.' to distinguish them)
- After marking, we know that:
- Any '.' was connected to the boundary β change back to 'O'
- Any remaining 'O' was not connected to the boundary β must be surrounded β change to 'X'
The beauty of this approach is that we only need to traverse the board twice - once for the DFS marking from boundaries, and once for the final conversion. We avoid the complexity of trying to determine if each region is surrounded by checking all possible escape routes.
Solution Approach
The implementation uses Depth-First Search (DFS) to mark all 'O's that are connected to the boundary. Here's how the solution works step by step:
1. DFS Helper Function
The dfs
function explores all connected 'O' cells starting from position (i, j)
:
- First checks if the current position is within bounds and contains an 'O'
- If valid, marks the cell with '.' (a temporary marker)
- Recursively explores all 4 adjacent cells (up, down, left, right)
The pairwise((-1, 0, 1, 0, -1))
creates pairs: (-1, 0)
, (0, 1)
, (1, 0)
, (0, -1)
representing the four directions.
2. Mark Boundary-Connected 'O's
Starting from all boundary cells:
- Left and right columns: For each row
i
, calldfs(i, 0)
anddfs(i, n-1)
- Top and bottom rows: For each column
j
, calldfs(0, j)
anddfs(m-1, j)
This ensures we explore all 'O's connected to any boundary cell, marking them with '.'.
3. Final Conversion
Traverse the entire board one more time:
- If a cell contains '.', it was connected to the boundary β change it back to 'O'
- If a cell still contains 'O', it was never reached from the boundary β it's surrounded β change it to 'X'
- 'X' cells remain unchanged
Time and Space Complexity:
- Time:
O(m Γ n)
where m and n are the dimensions of the board. We visit each cell at most twice. - Space:
O(m Γ n)
in the worst case for the recursion stack (if all cells are 'O' and connected).
The algorithm modifies the board in-place, using the board itself as a visited marker (by temporarily changing 'O' to '.'), which eliminates the need for a separate visited array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Initial Board:
X X X X X O O X X X O X X O X X
Step 1: Check boundary cells and mark connected 'O's
Starting from the boundaries:
- Top row (row 0): All 'X', nothing to mark
- Bottom row (row 3): Found 'O' at position (3,1)
- Left column (column 0): All 'X', nothing to mark
- Right column (column 3): All 'X', nothing to mark
When we find the 'O' at (3,1), we start DFS:
- Mark (3,1) as '.'
- Check its 4 neighbors:
- Up (2,1): It's 'X', stop
- Down: Out of bounds, stop
- Left (3,0): It's 'X', stop
- Right (3,2): It's 'X', stop
Board after marking boundary-connected 'O's:
X X X X X O O X X X O X X . X X
The 'O' at (3,1) is now marked as '.' because it touches the boundary. The 'O's at positions (1,1), (1,2), and (2,2) remain as 'O' because they're not connected to any boundary.
Step 2: Final conversion
Traverse the entire board:
- Position (0,0): 'X' β remains 'X'
- Position (0,1): 'X' β remains 'X'
- ...continuing for all 'X' cells...
- Position (1,1): 'O' β not connected to boundary β change to 'X'
- Position (1,2): 'O' β not connected to boundary β change to 'X'
- Position (2,2): 'O' β not connected to boundary β change to 'X'
- Position (3,1): '.' β was connected to boundary β change back to 'O'
Final Board:
X X X X X X X X X X X X X O X X
The three 'O's that formed an island in the middle were surrounded and converted to 'X'. The 'O' at the bottom edge remained 'O' because it touched the boundary.
Solution Implementation
1class Solution:
2 def solve(self, board: List[List[str]]) -> None:
3 """
4 Captures all regions of 'O' that are completely surrounded by 'X'.
5 Only 'O' regions connected to the border remain uncaptured.
6 """
7 # Get board dimensions
8 rows, cols = len(board), len(board[0])
9
10 def mark_border_connected(row: int, col: int) -> None:
11 """
12 DFS to mark all 'O' cells connected to borders with a temporary marker '.'
13 """
14 # Check boundaries and if current cell is 'O'
15 if not (0 <= row < rows and 0 <= col < cols and board[row][col] == "O"):
16 return
17
18 # Mark current cell as visited (border-connected)
19 board[row][col] = "."
20
21 # Explore all four directions: up, right, down, left
22 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
23 for dx, dy in directions:
24 mark_border_connected(row + dx, col + dy)
25
26 # Step 1: Mark all 'O' cells connected to borders
27 # Check left and right borders
28 for row in range(rows):
29 mark_border_connected(row, 0) # Left border
30 mark_border_connected(row, cols - 1) # Right border
31
32 # Check top and bottom borders
33 for col in range(cols):
34 mark_border_connected(0, col) # Top border
35 mark_border_connected(rows - 1, col) # Bottom border
36
37 # Step 2: Convert cells based on their current state
38 for row in range(rows):
39 for col in range(cols):
40 if board[row][col] == ".":
41 # Border-connected 'O' cells remain as 'O'
42 board[row][col] = "O"
43 elif board[row][col] == "O":
44 # Surrounded 'O' cells become 'X'
45 board[row][col] = "X"
46 # 'X' cells remain unchanged
47
1class Solution {
2 // Direction vectors for up, right, down, left movements
3 private final int[] DIRECTIONS = {-1, 0, 1, 0, -1};
4 private char[][] board;
5 private int rows;
6 private int cols;
7
8 /**
9 * Captures all regions of 'O' that are surrounded by 'X'.
10 * Only 'O' cells connected to the border remain unchanged.
11 *
12 * @param board 2D board containing 'X' and 'O' characters
13 */
14 public void solve(char[][] board) {
15 rows = board.length;
16 cols = board[0].length;
17 this.board = board;
18
19 // Mark all 'O' cells connected to left and right borders
20 for (int row = 0; row < rows; row++) {
21 dfs(row, 0); // Left border
22 dfs(row, cols - 1); // Right border
23 }
24
25 // Mark all 'O' cells connected to top and bottom borders
26 for (int col = 0; col < cols; col++) {
27 dfs(0, col); // Top border
28 dfs(rows - 1, col); // Bottom border
29 }
30
31 // Convert cells based on their marks:
32 // '.' (marked) -> 'O' (these are border-connected, safe cells)
33 // 'O' (unmarked) -> 'X' (these are surrounded cells to be captured)
34 for (int row = 0; row < rows; row++) {
35 for (int col = 0; col < cols; col++) {
36 if (board[row][col] == '.') {
37 board[row][col] = 'O';
38 } else if (board[row][col] == 'O') {
39 board[row][col] = 'X';
40 }
41 }
42 }
43 }
44
45 /**
46 * Performs depth-first search to mark all 'O' cells connected to position (row, col).
47 * Marks connected 'O' cells with '.' to indicate they're safe from capture.
48 *
49 * @param row Current row index
50 * @param col Current column index
51 */
52 private void dfs(int row, int col) {
53 // Check boundaries and if current cell is 'O'
54 if (row < 0 || row >= rows || col < 0 || col >= cols || board[row][col] != 'O') {
55 return;
56 }
57
58 // Mark current cell as visited and safe from capture
59 board[row][col] = '.';
60
61 // Explore all four adjacent cells (up, right, down, left)
62 for (int k = 0; k < 4; k++) {
63 int newRow = row + DIRECTIONS[k];
64 int newCol = col + DIRECTIONS[k + 1];
65 dfs(newRow, newCol);
66 }
67 }
68}
69
1class Solution {
2public:
3 void solve(vector<vector<char>>& board) {
4 // Get board dimensions
5 int rows = board.size();
6 int cols = board[0].size();
7
8 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
9 int directions[5] = {-1, 0, 1, 0, -1};
10
11 // DFS function to mark connected 'O' cells starting from border
12 function<void(int, int)> depthFirstSearch = [&](int row, int col) {
13 // Check boundaries and if current cell is 'O'
14 if (row < 0 || row >= rows || col < 0 || col >= cols || board[row][col] != 'O') {
15 return;
16 }
17
18 // Mark current 'O' as temporary '.' to indicate it's connected to border
19 board[row][col] = '.';
20
21 // Explore all 4 adjacent cells
22 for (int k = 0; k < 4; ++k) {
23 depthFirstSearch(row + directions[k], col + directions[k + 1]);
24 }
25 };
26
27 // Process left and right borders - mark all 'O's connected to borders
28 for (int i = 0; i < rows; ++i) {
29 depthFirstSearch(i, 0); // Left border
30 depthFirstSearch(i, cols - 1); // Right border
31 }
32
33 // Process top and bottom borders (excluding corners already processed)
34 for (int j = 1; j < cols - 1; ++j) {
35 depthFirstSearch(0, j); // Top border
36 depthFirstSearch(rows - 1, j); // Bottom border
37 }
38
39 // Final pass: convert marked cells and flip surrounded regions
40 for (auto& row : board) {
41 for (auto& cell : row) {
42 if (cell == '.') {
43 // Restore border-connected 'O's back to 'O'
44 cell = 'O';
45 } else if (cell == 'O') {
46 // Flip surrounded 'O's to 'X'
47 cell = 'X';
48 }
49 }
50 }
51 }
52};
53
1/**
2 * Solves the "Surrounded Regions" problem by capturing all 'O' regions that are surrounded by 'X'.
3 * Only 'O' regions connected to the board edges remain uncaptured.
4 * @param board - 2D array representing the game board with 'X' and 'O' characters
5 */
6function solve(board: string[][]): void {
7 // Get board dimensions
8 const rows: number = board.length;
9 const cols: number = board[0].length;
10
11 // Direction vectors for traversing up, right, down, left
12 // Using pattern: [row-1, col], [row, col+1], [row+1, col], [row, col-1]
13 const directions: number[] = [-1, 0, 1, 0, -1];
14
15 /**
16 * Depth-first search to mark all 'O' cells connected to the current cell
17 * @param row - Current row index
18 * @param col - Current column index
19 */
20 const dfs = (row: number, col: number): void => {
21 // Check boundaries and if current cell is 'O'
22 if (row < 0 || row >= rows || col < 0 || col >= cols || board[row][col] !== 'O') {
23 return;
24 }
25
26 // Mark current 'O' cell as visited using temporary marker '.'
27 board[row][col] = '.';
28
29 // Explore all four adjacent cells
30 for (let k = 0; k < 4; k++) {
31 const nextRow: number = row + directions[k];
32 const nextCol: number = col + directions[k + 1];
33 dfs(nextRow, nextCol);
34 }
35 };
36
37 // Mark all 'O' cells connected to left and right borders
38 for (let i = 0; i < rows; i++) {
39 dfs(i, 0); // Left border
40 dfs(i, cols - 1); // Right border
41 }
42
43 // Mark all 'O' cells connected to top and bottom borders
44 for (let j = 0; j < cols; j++) {
45 dfs(0, j); // Top border
46 dfs(rows - 1, j); // Bottom border
47 }
48
49 // Final pass: convert markers to final values
50 for (let i = 0; i < rows; i++) {
51 for (let j = 0; j < cols; j++) {
52 if (board[i][j] === '.') {
53 // Cells marked with '.' were connected to border, keep as 'O'
54 board[i][j] = 'O';
55 } else if (board[i][j] === 'O') {
56 // Unmarked 'O' cells are surrounded, capture them as 'X'
57 board[i][j] = 'X';
58 }
59 // 'X' cells remain unchanged
60 }
61 }
62}
63
Time and Space Complexity
Time Complexity: O(m Γ n)
where m
is the number of rows and n
is the number of columns in the board.
The algorithm performs the following operations:
- DFS calls from the border cells: The DFS is initiated from all border cells, which are
2m + 2n - 4
cells (approximatelyO(m + n)
). - Each cell in the board is visited at most once during the DFS traversal, as once a cell is marked as
"."
, it won't be revisited. - The final nested loop to convert cells runs through all
m Γ n
cells exactly once. - Therefore, the overall time complexity is
O(m Γ n)
.
Space Complexity: O(m Γ n)
where m
is the number of rows and n
is the number of columns in the board.
The space complexity comes from:
- The recursive DFS call stack: In the worst case, if all cells are connected
"O"
s starting from a border, the recursion depth could beO(m Γ n)
. This happens when the DFS traverses through all cells in a snake-like pattern. - No additional data structures are used besides the implicit recursion stack.
- Therefore, the space complexity is
O(m Γ n)
due to the recursion stack depth.
Common Pitfalls
1. Stack Overflow with Deep Recursion
The recursive DFS implementation can cause a stack overflow when dealing with large boards where most cells are 'O' and connected. Python's default recursion limit (typically 1000) can be exceeded.
Solution: Convert the recursive DFS to an iterative approach using an explicit stack:
def mark_border_connected(row: int, col: int) -> None:
stack = [(row, col)]
while stack:
r, c = stack.pop()
if not (0 <= r < rows and 0 <= c < cols and board[r][c] == "O"):
continue
board[r][c] = "."
for dx, dy in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
stack.append((r + dx, c + dy))
2. Forgetting to Check All Border Cells
A common mistake is only checking corners or missing some border cells, especially when the board has only one row or one column.
Solution: Ensure your border traversal covers all edge cases:
- For a 1Γn or mΓ1 board, some cells are on multiple borders simultaneously
- The current implementation handles this correctly by checking all four borders separately
3. Using the Same Marker as Original Values
Using 'O' or 'X' as temporary markers during DFS can cause cells to be processed multiple times or skipped entirely.
Solution: Always use a distinct temporary marker (like '.' in the solution) that doesn't conflict with the original values.
4. Modifying Board During Iteration
Attempting to capture surrounded regions while simultaneously exploring from borders can lead to incorrect results.
Solution: Follow the two-phase approach:
- First, mark all border-connected cells completely
- Then, perform the final conversion in a separate pass
5. Incorrect Direction Vectors
Hardcoding direction vectors incorrectly (e.g., [(1, 0), (0, 1), (-1, 0), (0, -1)]
in wrong order or with wrong values) won't cause obvious errors but might miss some connected cells.
Solution: Define directions clearly and consistently:
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] # up, right, down, left # Or use named tuples for clarity UP, RIGHT, DOWN, LEFT = (-1, 0), (0, 1), (1, 0), (0, -1)
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!