Problem overview
The method Solve receives board, a char[][]: an array of rows, each row an array of 'X' and 'O' characters. Two 'O' cells are connected when one is directly above, below, left or right of the other. Diagonal cells are not connected. A region is a group of connected 'O' cells. A region is surrounded when none of its cells is on the edge of the board (first or last row, first or last column). The method must change every surrounded region to 'X', in place, and return nothing. A region with a cell on the edge stays as it is.
In this board:
X X X X
X O O X
X X O X
X O X X
the three 'O' cells in the middle form a region that touches no edge, so they are captured. The 'O' in the last row is on the edge, so it stays:
X X X X
X X X X
X X X X
X O X X
Approach: start from the edge, not from the inside
The direct way is to take each 'O' and search for a path of 'O' cells to the edge. That can repeat a lot of work: one big region in the middle gets searched again from each of its cells, which can cost O((m * n)^2).
Turning the question around fixes this. An 'O' on the edge is never captured, and neither is any 'O' connected to one that is safe. So start from every 'O' on the edge at once, and spread inward through neighbouring 'O' cells, marking each one as safe. When the spreading ends, every 'O' that is not marked has no path to the edge, so its region is surrounded. One more pass over the board changes those cells to 'X'.
The spreading is a breadth-first search (BFS) with a queue. A queue also keeps the search free of recursion, so a region that winds through the whole board can't exhaust the call stack.
A tempting shortcut is to capture each 'O' whose four neighbours are all 'X'. That fails: on the board above it captures nothing, because each 'O' of the region has another 'O' as a neighbour. Connected cells must be treated as one region.
Step by step
- Read the size.
rowsisboard.Length, andcolsisboard[0].Length, since every row has the same length. - Create the marks.
bool[,] safe = new bool[rows, cols]is a rectangular array that starts allfalse. A cell becomestruewhen it is an'O'connected to the edge. - Create the queue of
(int, int)tuples, the (row, column) cells whose neighbours are still to be checked. - Find the edge cells. Two nested loops go over every cell.
onBorderis true for the first or last row and the first or last column. When a cell is on the border and holds'O', mark it insafeand enqueue it. - List the four moves as tuples:
(-1, 0),(1, 0),(0, -1)and(0, 1). Each changes the row or the column, never both, so diagonal neighbours are not reached. - Run the BFS. While the queue isn't empty, dequeue a cell and unpack it into
rowandcol. - Visit the neighbours. For each move compute
nextRowandnextCol. If the position is inside the board, and the cell holds'O'and isn't marked yet, mark it and enqueue it. Marking when a cell joins the queue means no cell is enqueued twice. - Capture. Go over the board once more. Every cell that holds
'O'and isn't marked safe becomes'X'.
The solution in C#
class Solution {
public void Solve(char[][] board) {
int rows = board.Length;
int cols = board[0].Length;
bool[,] safe = new bool[rows, cols];
Queue<(int, int)> queue = new Queue<(int, int)>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
bool onBorder = r == 0 || r == rows - 1 || c == 0 || c == cols - 1;
if (onBorder && board[r][c] == 'O') {
safe[r, c] = true;
queue.Enqueue((r, c));
}
}
}
(int, int)[] directions = { (-1, 0), (1, 0), (0, -1), (0, 1) };
while (queue.Count > 0) {
(int row, int col) = queue.Dequeue();
foreach ((int dr, int dc) in directions) {
int nextRow = row + dr;
int nextCol = col + dc;
if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
if (board[nextRow][nextCol] == 'O' && !safe[nextRow, nextCol]) {
safe[nextRow, nextCol] = true;
queue.Enqueue((nextRow, nextCol));
}
}
}
}
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (board[r][c] == 'O' && !safe[r, c]) {
board[r][c] = 'X';
}
}
}
}
}
The C# details worth knowing:
- Jagged and rectangular arrays together.
char[][]is a jagged array, an array of arrays, indexedboard[r][c].bool[,]is a rectangular array, indexedsafe[r, c]and created withnew bool[rows, cols]. The two index syntaxes are easy to mix up, and the compiler rejects the wrong one. - Tuples.
(int, int)is a value tuple type: two values kept together.queue.Enqueue((r, c))builds one, and(int row, int col) = queue.Dequeue();deconstructs it into two variables. The same deconstruction works in theforeachoverdirections, which givesdranddc. - Array initializer with tuples.
(int, int)[] directions = { (-1, 0), ... };declares and fills the array in one statement. - Characters.
'O'in single quotes is achar, the type of every cell. A string such as"O"would not compile in a comparison with achar. - In-place change.
boardis a reference to the caller's arrays, so assigningboard[r][c] = 'X'changes the board the caller passed. That is why the method returnsvoid. - Bounds are checked before indexing. Reading
board[-1][0]would throw an exception, so the neighbour's position is tested first.
Complexity
Let the board have m rows and n columns.
Time: O(m * n). The first pass and the last pass visit each cell once. In the BFS each cell is enqueued at most once, and handling it looks at four neighbours.
Space: O(m * n). The safe array has one entry per cell, and the queue can hold many cells at once.
Edge cases and common mistakes
- A single cell.
Oalone is on every edge, so it stays. - Thin boards. With one or two rows, or one or two columns, every cell is on the border, so nothing is captured:
X X X X / X O O O / X X X Xis unchanged. - A lone inner
'O'. A 3 by 3 board with an'O'in the middle and'X'everywhere else turns to all'X'. - A ring. In a 5 by 5 board whose inner 3 by 3 block is a ring of
'O'around an'X', the whole ring is captured, since it touches no edge. - Diagonal contact. A cell that touches an edge-connected region only at a corner is not part of it. In
OXXXX / XOOOX / XXXOX / XOXOX / XXXOXthe'O'in the top-left corner touches the region below it only diagonally, and it stays because it is on the edge itself, while the lone'O'at row 3, column 1 (counting from 0) is captured.
Testing
The method changes the board it receives. These boards, written row by row, give:
XXXX / XOOX / XXOX / XOXXbecomesXXXX / XXXX / XXXX / XOXX.OstaysO.OXXXX / XOOOX / XXXOX / XOXOX / XXXOXbecomesOXXXX / XOOOX / XXXOX / XXXOX / XXXOX.XXXXX / XOOOX / XOXOX / XOOOX / XXXXXbecomes allX.XXXX / XOOO / XXXXis unchanged: the'O'cells are in the last column.XXX / XOX / XXXbecomes allX.
