Problem overview
The method solve receives board, a grid of m rows and n columns of Char values, each 'X' or 'O'. It returns nothing: it must change board itself.
Two 'O' cells are connected when they touch side by side (up, down, left or right, never diagonally). A region is a group of connected 'O' cells. A region is surrounded when none of its cells lies on the border of the board, which means the first or last row, or the first or last column. Every surrounded region is captured: all its cells become 'X'. A region with at least one cell on the border stays as it is.
In the lesson's first example, the board has three 'O' cells in the middle that form one region, and none of them is on the border, so they are captured. The 'O' in the last row stays. The board X X X X / X O O X / X X O X / X O X X becomes X X X X / X X X X / X X X X / X O X X.
Approach: find what is safe, capture the rest
The direct idea is to take each 'O' and search outward to see whether its region reaches the border. That repeats the same search many times: one big region in the middle of the board gets searched again from each of its cells, which can cost time quadratic in the number of cells.
Turn the question around. Every 'O' on the border is safe, and so is every 'O' connected to a safe one. So start from all the border 'O' cells at once and spread inward with a breadth-first search, marking every 'O' reached. When the search ends, the marked cells are exactly the cells of the regions that touch the border. Any 'O' still unmarked belongs to a surrounded region, and one more pass over the board turns it into 'X'.
Marking a cell as soon as it joins the queue, not when it leaves, means no cell is queued twice, so every cell is processed at most once.
The search uses a queue instead of recursion on purpose. A single region can wind through the whole board, and a recursive search that goes tens of thousands of calls deep can overflow the call stack. The queue lives on the heap and has no such limit.
Step by step
- Read the size.
rowsisboard.sizeandcolsisboard[0].size. All rows have the same length. - Create
safe, a grid of booleans with the same shape as the board. A cell is markedtruewhen it holds an'O'connected to the border. Nothing is marked at the start. - Create
queue, a queue ofPair<Int, Int>values holding (row, column) positions of safe cells whose neighbours still have to be checked. - Loop over the rows and, inside, the columns to find the border cells.
- Compute
onBorderwith four tests joined by||: first row, last row, first column or last column. - Seed the queue. When the cell is on the border and holds an
'O', mark it insafeand add its position to the queue. - Define
directions, the four moves: up, down, left, right. No move changes both the row and the column, so diagonal cells are never neighbours. - Run the search while the queue is not empty. Take the position at the front and unpack it into
rowandcol. - Try each direction. Compute
nextRowandnextCol. - Check bounds. A border cell has neighbours outside the board, and those are skipped.
- Spread. If the neighbour holds an
'O'and is not yet marked, mark it and add its position to the queue. - Capture. After the search, go over the board again. Every
'O'that is not marked safe becomes'X'.
The solution in Kotlin
class Solution {
fun solve(board: Array<CharArray>) {
val rows = board.size
val cols = board[0].size
val safe = Array(rows) { BooleanArray(cols) }
val queue = ArrayDeque<Pair<Int, Int>>()
for (r in 0 until rows) {
for (c in 0 until cols) {
val onBorder = r == 0 || r == rows - 1 || c == 0 || c == cols - 1
if (onBorder && board[r][c] == 'O') {
safe[r][c] = true
queue.add(Pair(r, c))
}
}
}
val directions = arrayOf(Pair(-1, 0), Pair(1, 0), Pair(0, -1), Pair(0, 1))
while (queue.isNotEmpty()) {
val (row, col) = queue.removeFirst()
for ((dr, dc) in directions) {
val nextRow = row + dr
val 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.add(Pair(nextRow, nextCol))
}
}
}
}
for (r in 0 until rows) {
for (c in 0 until cols) {
if (board[r][c] == 'O' && !safe[r][c]) {
board[r][c] = 'X'
}
}
}
}
}
Kotlin details worth knowing:
- The board is modified in place.
Array<CharArray>is a mutable array of mutable rows, passed by reference. Assigningboard[r][c] = 'X'changes the caller's board, which is why the method needs no return value. - Chars use single quotes.
'O'is aChar, and each cell of aCharArrayis one. - Building
safe.Array(rows) { BooleanArray(cols) }runs the block once per row, so each row is a separate array. WritingArray(rows, BooleanArray(cols))instead does not compile:argument type mismatch: actual type is 'BooleanArray', but 'Function1<Int, T (of class Array<T>)>' was expected. A newBooleanArraystarts allfalse. - Destructuring. A
Paircan be unpacked, as inval (row, col) = queue.removeFirst()andfor ((dr, dc) in directions). Both give each half of the pair its own name. ArrayDequeis Kotlin's own deque:addputs an element at the back andremoveFirst()takes one from the front, both in constant time.
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 added, and looks at four neighbours. The last pass visits each cell once.
Space: O(m · n). The safe grid has one entry per cell, and the queue can hold a large part of the board at once.
Edge cases and common mistakes
- One cell. A board holding just
OstaysO: the cell is on every border. A boardX X X / X O X / X X Xbecomes allX. - A board of only
'O'. A 3 by 3 board of'O'cells stays unchanged: the whole board is one region, and it touches the border. - Diagonal contact doesn't connect. In the lesson's third example, the
'O'in the top-left corner touches the middle region only diagonally. It is on the border anyway, so it stays. On the boardX O X / O X O / X O X, the four'O'cells touch each other only diagonally, and each is on the border, so nothing changes. - Only keeping border cells. Checking only whether a cell is on the border, without spreading, changes the lesson's third example into
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. The middle cells of the region that reaches the border are wrongly captured, because they are not on the border themselves. The search has to follow the connections. - Large regions. A 200 by 200 board that is all
'O'stays unchanged, and a long winding region in the middle of a 200 by 200 board is captured completely when it is closed (all 19,701 of its cells in a test), or kept completely when a single cell of it touches the border.
Testing
X X X X / X O O X / X X O X / X O X XbecomesX X X X / X X X X / X X X X / X O X X.OstaysO.O X X X X / X O O O X / X X X O X / X O X O X / X X X O XbecomesO X X X X / X O O O X / X X X O X / X X X O X / X X X O X.X X X X X / X O O O X / X O X O X / X O O O X / X X X X X, a ring of'O'cells, becomes allX.O O O / O O O / O O Ostays unchanged.
