Problem overview
The method solve receives board, a grid of m rows and n columns made of the characters "X" and "O", as an array of rows of Character values. Two "O" cells are connected when one is directly above, below, left or right of the other; diagonal neighbours don't count. A region is a group of "O" cells connected to each other and to no other "O".
A region is surrounded when none of its cells lies on the edge of the board (first or last row, first or last column). The method must change every cell of every surrounded region to "X". A region with a cell on the edge stays as it is. The board is changed in place and nothing is returned: the parameter is inout [[Character]].
Take this board:
XXXX
XOOX
XXOX
XOXX
The three cells (1,1), (1,2) and (2,2) form one region and none of them is on the edge, so they are captured. The "O" at (3,1) is in the last row, so it stays. The board becomes:
XXXX
XXXX
XXXX
XOXX
Approach: start from the edge and mark what is safe
The direct approach checks each "O" region for a path to the edge. A big region in the middle of the board would be searched again from every one of its cells, which can cost O((m · n)^2).
The idea is to turn the question around. Every "O" on the edge is safe, and so is every "O" connected to a safe one, because they belong to the same region. A breadth-first search that starts from all the edge "O"s at once marks every safe cell, and each cell enters the queue at most once. After it, 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 it into "X".
A queue is used instead of recursion. A region can wind through the whole board, and a recursive search that goes tens of thousands of calls deep can run out of call stack.
A tempting shortcut is to flip every "O" that is not itself on the edge. On this board:
OXXXX
XOOOX
XXXOX
XOXOX
XXXOX
it would leave only the corner "O" and the last "O" of the bottom row and capture the whole middle region, even though that region reaches the last row through its bottom cell. The correct result keeps all of it:
OXXXX
XOOOX
XXXOX
XXXOX
XXXOX
Only the lone "O" at (3,1) is closed in on all four sides, so it becomes "X". The corner "O" touches the middle region only diagonally, but it is on the edge itself.
Step by step
- Read the size. Store
board.countinrowsandboard[0].countincols. Every row has the same length. - Create
safe, a grid offalsevalues of the same size, by repeating a row ofcolsfalsesrowstimes. A cell is markedtruewhen it holds an"O"connected to the edge. - Create
queue, an empty array of(Int, Int)tuples. It holds the safe cells whose neighbours are still to be checked. - Loop over the rows with
rand, inside, over the columns withc. This first pass finds the"O"s on the edge. - Test for the edge. The constant
onBorderis true whenr == 0,r == rows - 1,c == 0orc == cols - 1. - Start from edge
"O"s. WhenonBorder && board[r][c] == "O", setsafe[r][c]totrueand append(r, c)toqueue. A cell is marked as it joins the queue, so no other cell adds it a second time. - Define the four moves in
directions: up(-1, 0), down(1, 0), left(0, -1), right(0, 1). Each move changes only one coordinate, so diagonals are never neighbours. - Create
head, starting at 0: the index of the next cell to take from the queue. Moving it forward takes a cell out of the queue without shifting the others, whichremoveFirst()would do. - Loop while
head < queue.count. Each turn takes one safe cell and checks its four neighbours. - Take the cell: unpack
queue[head]into(row, col)and dohead += 1. - Loop over
directions, unpacking each move into(dr, dc), and computenextRowandnextCol. - Stay inside the board. Check that both coordinates are at least 0 and below
rowsorcolsbefore indexing with them. - Spread the mark. If the neighbour holds
"O"and is not safe yet, set it safe and append it to the queue. - Capture. After the search, loop over the whole board again. Every cell that holds
"O"and is not safe is set to"X".
The solution in Swift
class Solution {
func solve(_ board: inout [[Character]]) {
let rows = board.count
let cols = board[0].count
var safe = [[Bool]](repeating: [Bool](repeating: false, count: cols), count: rows)
var queue = [(Int, Int)]()
for r in 0..<rows {
for c in 0..<cols {
let onBorder = r == 0 || r == rows - 1 || c == 0 || c == cols - 1
if onBorder && board[r][c] == "O" {
safe[r][c] = true
queue.append((r, c))
}
}
}
let directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
var head = 0
while head < queue.count {
let (row, col) = queue[head]
head += 1
for (dr, dc) in directions {
let nextRow = row + dr
let 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.append((nextRow, nextCol))
}
}
}
}
for r in 0..<rows {
for c in 0..<cols {
if board[r][c] == "O" && !safe[r][c] {
board[r][c] = "X"
}
}
}
}
}
The Swift details worth a closer look:
inout. Parameters are constants and arrays are copied when passed. Aninoutparameter letssolvechange the caller's board, which is why the method returns nothing. To call it, the caller writessolution.solve(&grid)with avargrid.- Characters. The cells are
Charactervalues. A double-quoted literal such as"O"becomes aCharacterwhen it is compared with a cell or assigned to one. - Independent rows.
[[Bool]](repeating: [Bool](repeating: false, count: cols), count: rows)repeats one row, but arrays are value types, so each row is its own copy and marking one cell changes only that row. In a language where arrays are references, the same trick would share one row. - Tuples.
queueholds(Int, Int)pairs.let (row, col) = queue[head]andfor (dr, dc) in directionsunpack them directly. - The nested
if. The bounds check has to come first. The neighbour is read withboard[nextRow][nextCol]only inside it, because an index outside the array stops the program at run time in Swift.
Complexity
Time: O(m · n). The first pass visits each cell once. Each cell joins the queue at most once, because it is marked when it joins, and it looks at 4 neighbours when it leaves. The last pass visits each cell once.
Space: O(m · n). The safe grid has one entry per cell, and the queue can hold up to every cell of the board.
Edge cases and common mistakes
- A single cell. The board
["O"]has one cell that is on every edge, so it stays"O". - Thin boards. When a board has one or two rows or columns, every cell is on the edge, so nothing is captured.
- A ring around an
"X". The boardOOO / OXO / OOOis unchanged: every"O"is on the edge. - A lone interior
"O". The boardXXX / XOX / XXXbecomes all"X". - Diagonal neighbours. Adding diagonal moves to
directionswould join cells that the problem keeps apart: two"O"s that only touch at a corner are not connected. - Checking only edge cells. An
"O"next to the edge is not safe on its own. Safety spreads through connected"O"s, which is why a search is needed. - Marking when taken out. If a cell is marked only when it leaves the queue, it can be appended several times by different neighbours. Mark it as it joins.
- Bounds. Checking
nextRow < rowsandnextCol < colsbut forgetting the lower bounds of 0 crashes on cells in the first row or column.
Testing
XXXX / XOOX / XXOX / XOXXbecomesXXXX / XXXX / XXXX / XOXX.OstaysO.OXXXX / XOOOX / XXXOX / XOXOX / XXXOXbecomesOXXXX / XOOOX / XXXOX / XXXOX / XXXOX.XXX / XOX / XXXbecomesXXX / XXX / XXX.OOO / OXO / OOOstays the same.
