Problem overview
The function solve receives board, a &mut Vec<Vec<char>>: a grid of m rows and n columns in which every cell holds 'X' or 'O'. Two 'O' cells are connected when one is directly above, below, left or right of the other. Diagonal neighbours do not count. A region is a group of 'O' cells that reach each other through connected cells.
A region is surrounded when none of its cells lies on the edge of the board, that is, in the first or last row or the first or last column. The function must change every surrounded region to 'X', in place, and return nothing. A region with at least one cell on the edge stays as it is.
In the first example, the board is
X X X X
X O O X
X X O X
X O X X
The three 'O' cells at the top form one region with no cell on the edge, so they become 'X'. The 'O' in the last row is on the edge and stays:
X X X X
X X X X
X X X X
X O X X
The statement asks for O(m * n) time and O(m * n) extra space.
Approach: start from the edge, not from the regions
The obvious question is "does this region reach the edge?", asked once per region. A cell-by-cell version of that question can search the same large region again from each of its cells, which can cost O((m * n)^2). Capturing every interior 'O' without searching is wrong as well. On this board
O X X X X
X O O O X
X X X O X
X O X O X
X X X O X
a version that turns every 'O' not on the edge into 'X' returns
O X X X X
X X X X X
X X X X X
X X X X X
X X X O X
which wrongly captures the winding region in the middle: it reaches the edge through the 'O' in the last row. Only the 'O's on the edge survive.
The way out is to turn the question around. Every 'O' on the edge is safe, and so is every 'O' connected to a safe one. So begin with all the edge 'O's at once and spread inward with a breadth-first search (BFS), marking every cell it reaches. Each cell joins the queue at most once. When the search ends, every 'O' that is not marked has no path of 'O's to the edge, so its region is surrounded, and one more pass over the board turns those cells into 'X'.
On the board above, the edge pass queues the two edge cells (0, 0) and (4, 3), and the search then marks the middle region, one cell at a time. The marked cells, shown as S, are:
S....
.SSS.
...S.
...S.
...S.
The one unmarked 'O', at row 3, column 1, is the surrounded cell. It becomes 'X', and the result is
O X X X X
X O O O X
X X X O X
X X X O X
X X X O X
The lesson uses a queue rather than recursion, because a single region can wind through the whole board, and a recursive search that deep can run out of call stack.
Step by step
- Read the size.
rowsisboard.len()andcolsisboard[0].len(). All rows have the same length. - Create the
safegrid withvec![vec![false; cols]; rows]. The innervec!makes one row, and the outer one repeats itrowstimes, each with its own copy. At the start nothing is marked. - Create the queue as a
VecDeque<(usize, usize)>. It holds the safe cells whose neighbours are still to be checked, as (row, column) tuples. - Loop over rows and columns with two nested
forloops, to find the'O'cells on the edge. - Test for the edge.
on_borderis true for the first or last row or the first or last column, joined with||. - Seed the search. If the cell is on the edge and holds an
'O', setsafe[r][c]totrueandpush_back((r, c)). Marking at the moment of queueing means no other cell can add it a second time. - Describe the four moves as an array of (row change, column change) tuples of type
[(i32, i32); 4]. They arei32because a move can be negative. - Run the search with
while let Some((row, col)) = queue.pop_front(). It ends when the queue is empty. - Visit each neighbour. Loop over
&directionswith the pattern&(dr, dc), which copies the two numbers out of the array. - Compute the neighbour in
i32:next_row = row as i32 + drandnext_col = col as i32 + dc. - Check that it is on the board: both values at least 0 and below
rows as i32orcols as i32. Ini32, a step off the top or the left edge shows up as -1, which the check can see. - Convert back to indexes with
as usize, shadowing the two names in one tuple. - Mark and queue the neighbour if it holds an
'O'and is not marked yet: setsafetotrueand push it. - Capture. After the search, loop over the board again and turn each
'O'that is not marked safe into'X'.
The solution in Rust
use std::collections::VecDeque;
impl Solution {
fn solve(board: &mut Vec<Vec<char>>) {
let rows = board.len();
let cols = board[0].len();
let mut safe = vec![vec![false; cols]; rows];
let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
for r in 0..rows {
for c in 0..cols {
let on_border = r == 0 || r == rows - 1 || c == 0 || c == cols - 1;
if on_border && board[r][c] == 'O' {
safe[r][c] = true;
queue.push_back((r, c));
}
}
}
let directions: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
while let Some((row, col)) = queue.pop_front() {
for &(dr, dc) in &directions {
let next_row = row as i32 + dr;
let next_col = col as i32 + dc;
if next_row >= 0 && next_row < rows as i32 && next_col >= 0 && next_col < cols as i32 {
let (next_row, next_col) = (next_row as usize, next_col as usize);
if board[next_row][next_col] == 'O' && !safe[next_row][next_col] {
safe[next_row][next_col] = true;
queue.push_back((next_row, next_col));
}
}
}
}
for r in 0..rows {
for c in 0..cols {
if board[r][c] == 'O' && !safe[r][c] {
board[r][c] = 'X';
}
}
}
}
}
The Rust details worth a closer look:
&mut Vec<Vec<char>>. The function borrows the caller's board mutably. It changes the cells through the borrow, so the caller sees the result without anything being returned. Each cell is achar, written with single quotes:'O'is achar, while"O"would be a string.- Casting for the bounds check. A
usizecannot be negative. In a build with overflow checks,row - 1at row 0 panics, and in the optimized build the lessons use it wraps to a huge number. Converting toi32first makes-1a normal value that the range check can reject, andas usizeruns only after the check has passed. - Shadowing.
let (next_row, next_col) = (next_row as usize, next_col as usize);reuses the names for new variables of the index type, so the code below reads naturally. - The pattern
&(dr, dc). Iterating&directionsyields references to tuples. The&in the pattern copies the twoi32values out, sodranddcare plain numbers. while let.pop_front()returns anOption, and the loop runs as long as it matchesSome(...). The tuple is unpacked intorowandcol.- The grid of flags. The inner
vec![false; cols]is cloned by the outervec![...; rows], so the rows do not share storage. - Edge-of-board indexes.
rows - 1andcols - 1are safe because the constraints guarantee at least one row and one column.
Complexity
Time: O(m * n). The edge pass and the final pass each visit every cell once. Each cell joins the queue at most once and checks four neighbours with constant work.
Space: O(m * n). The safe grid has one flag per cell, and the queue holds at most all the cells.
Edge cases and common mistakes
- A single cell. The board
Ohas its only cell on every edge, so it staysO. A one-row board such asO O Ois entirely on the edge and nothing is captured. - An isolated interior cell. A
3 x 3board withOin the middle andXaround it becomes allX. - Capturing every interior
'O'. It ignores the connection to the edge, as the board above shows. - Counting diagonals as connections. With eight directions instead of four, on the board
O X X X X / X O X X X / X X X X Xthe interior'O'would stay, because it touches the edge'O'diagonally. The correct result turns it into'X'. - Edge
'O's that are not connected to each other. Seeding the queue with all of them is what makes the search start from every edge at once, not from one corner. - Marking when popping instead of when pushing. A cell could then join the queue several times before it is processed. The flag set before
push_backprevents that. - Recursive search. A single region can wind through a board of up to 200 x 200 cells, which makes a recursive version very deep. The queue keeps the depth out of the call stack.
Testing
- The board
XXXX / XOOX / XXOX / XOXXbecomesXXXX / XXXX / XXXX / XOXX. - The board
OstaysO. - The board
OXXXX / XOOOX / XXXOX / XOXOX / XXXOXbecomesOXXXX / XOOOX / XXXOX / XXXOX / XXXOX. - The board
XOX / OXO / XOXstays the same: each'O'is on the edge. - The board
XXX / XOX / XXXbecomes allX. - The board
XXXXX / XOOOX / XOXOX / XOOOX / XXXXXbecomes allX: the ring of'O's is one surrounded region.
