Any O region fully surrounded by Xs gets flipped. The trick: instead of asking "is this O surrounded?" — ask "is it reachable from a border?" Seed a DFS from every border O, mark what's safe, then flip everything else.
Given an m×n board of 'X' and 'O', flip every 'O' that is fully surrounded by 'X' in all four directions. An 'O' is safe if it touches — or connects to — a board edge.
Worked example — input:
X X X X X O O X X X O X X O X X
The three Os in the interior (rows 1–2, cols 1–2) form one component with no path to any border → they get flipped. The lone O at row 3 col 1 also has no border connection → flipped too. Output:
X X X X X X X X X X X X X X X X
Osurrounded?" requires knowing the entire component, which is expensive to verify per-cell. Instead, invert: an O is safe (not flipped) if and only if it can reach the board border via 4-directional O steps. Run DFS/BFS from every border O, tagging reachable cells as safe — everything left unmarked is surrounded.'O', launch a DFS. This reaches every O connected to the edge — they are all safe.'O' with a sentinel like 'S'so we don't revisit it and can distinguish it from the surrounded ones in step 3.'O' was never reachedfrom the border — it's surrounded. Flip it to 'X'.'S' back to 'O'.Ositting on the edge is safe by definition because it can't be "surrounded" when it already touches the boundary. Many learners forget to seed the DFS at the border cells themselves (not just their interior neighbors), leaving border Os incorrectly flipped.In-place modification (using the 'S' sentinel) means we need no separate visited array — the board itself tracks state. Space is O(m·n) in the worst case due to DFS recursion depth (a zigzag path of O's). An iterative BFS with an explicit queue has the same space bound but avoids stack overflow on large boards.
O to mark safe cells.1▶function solve(board: string[][]): void {2▶ const rows = board.length;3▶ const cols = board[0]?.length ?? 0;45 // DFS from a border cell, marking every reachable 'O' as safe ('S').6▶ function dfs(r: number, c: number): void {7 if (r < 0 || r >= rows || c < 0 || c >= cols) return;8 if (board[r][c] !== 'O') return;9 board[r][c] = 'S'; // mark safe10 dfs(r - 1, c);11 dfs(r + 1, c);12 dfs(r, c - 1);13 dfs(r, c + 1);14 }1516 // 1. Seed DFS from every 'O' on the four edges.17 for (let r = 0; r < rows; r++) {18 dfs(r, 0);19 dfs(r, cols - 1);20 }21 for (let c = 0; c < cols; c++) {22 dfs(0, c);23 dfs(rows - 1, c);24 }2526 // 2. Flip: 'O' → 'X' (surrounded), 'S' → 'O' (restore safe).27 for (let r = 0; r < rows; r++) {28 for (let c = 0; c < cols; c++) {29 if (board[r][c] === 'O') board[r][c] = 'X';30 else if (board[r][c] === 'S') board[r][c] = 'O';31 }32 }33}
function solve(board: string[][]): void {
const rows = board.length;
const cols = board[0]?.length ?? 0;
// DFS from a border cell, marking every reachable 'O' as safe ('S').
function dfs(r: number, c: number): void {
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
if (board[r][c] !== 'O') return;
board[r][c] = 'S'; // mark safe
dfs(r - 1, c);
dfs(r + 1, c);
dfs(r, c - 1);
dfs(r, c + 1);
}
// 1. Seed DFS from every 'O' on the four edges.
for (let r = 0; r < rows; r++) {
dfs(r, 0);
dfs(r, cols - 1);
}
for (let c = 0; c < cols; c++) {
dfs(0, c);
dfs(rows - 1, c);
}
// 2. Flip: 'O' → 'X' (surrounded), 'S' → 'O' (restore safe).
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (board[r][c] === 'O') board[r][c] = 'X';
else if (board[r][c] === 'S') board[r][c] = 'O';
}
}
}dfs(r, c) returns immediately if the cell is out of bounds or not an 'O'. Otherwise it marks the cell 'S' (safe sentinel) and recurses in all four directions. The early !== 'O' check also stops re-entry on already-marked 'S' cells and on 'X' borders.dfs on every cell in the leftmost and rightmost columns, and in the top and bottom rows. Any 'O' that can reach the border gets tagged. Crucially this is O(m+n) seed calls, and each internal cell is visited at most once overall.'O' it was unreachable from any border — flip it to 'X'. If it's 'S' (safe) restore it to 'O'. 'X' cells are left unchanged throughout.Os would blow the stack).Ointo a virtual "border" node, then a single pass identifies which Os are connected. Same asymptotic complexity, often more cache-friendly.dirs array to include the four diagonals. The rest of the algorithm is unchanged.O after the DFS phase (i.e., surrounded) before the flip step.O, BFS to check whether it can reach the border. O(m·n) per cell → O(m²n²) total. The border-seeding trick fuses all those searches into a single O(m·n) sweep.| "flip surrounded region / captured cells" | DFS from border edges |
| two-category graph (safe vs captured) | sentinel + two-pass flip |
| "reachable from boundary" type check | boundary-seeded flood fill |
| connected components touching / not touching edge | mark then sweep |
function solve(board: string[][]): void {
const rows = board.length, cols = board[0]?.length ?? 0;
function dfs(r: number, c: number): void {
if (r < 0 || r >= rows || c < 0 || c >= cols) return;
if (board[r][c] !== 'O') return;
board[r][c] = 'S';
dfs(r-1,c); dfs(r+1,c); dfs(r,c-1); dfs(r,c+1);
}
// seed from all four edges
for (let r = 0; r < rows; r++) { dfs(r,0); dfs(r,cols-1); }
for (let c = 0; c < cols; c++) { dfs(0,c); dfs(rows-1,c); }
// flip 'O'→'X', restore 'S'→'O'
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++) {
if (board[r][c] === 'O') board[r][c] = 'X';
else if (board[r][c] === 'S') board[r][c] = 'O';
}
}'O' cells — i.e., it is in the same component as the border.m×n board?Os get flipped?X X X X X O O X X X O X X O X X
Os. After running solve, what is the board?