Problem overview
The method solve receives board, an Array[Array[Char]] grid of m rows and n columns. Every cell is 'X' or 'O'. Two 'O' cells are connected when one is directly above, below, left or right of the other; diagonals don't count. A group of connected 'O' cells is a region.
A region is surrounded when none of its cells is on the edge of the board, that is, in the first or last row or the first or last column. The method must change every cell of every surrounded region to 'X'. A region with at least one cell on the edge stays as it is. The method returns Unit: the change is made in board itself.
Here is a board with 5 rows and 5 columns, and the same board after solve:
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
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 region in the middle winds down to the 'O' in the last row, so it touches the edge and survives. The 'O' in the top-left corner is on the edge itself. The 'O' in row 3, column 1 is closed in by 'X' on all four sides, so it becomes 'X'.
Approach: start from the edge instead of from each region
The direct way is to take each 'O' and search for a path of 'O' cells to the edge. That repeats work: one big region in the middle of the board is searched again from each of its cells, which can cost O((m * n)^2).
Turning the question around fixes it. An 'O' on the edge is safe, and so is every 'O' connected to a safe one. One breadth-first search that starts from all the border 'O' cells at once marks every safe cell, and each cell enters the queue at most once. An 'O' that was never marked has no path of 'O' cells to the edge, so its region is surrounded. A second pass over the board turns those cells into 'X'.
The search uses a queue rather than recursion. One region can wind through the whole board, and a recursive search that deep can exhaust the call stack.
Step by step
- Read the size:
rowsisboard.lengthandcolsisboard(0).length. Every row has the same length. - Create
safewithArray.fill(rows, cols)(false): a grid of booleans, allfalse. A cell becomestruewhen it holds an'O'connected to the edge. - Create
queue, an emptymutable.Queue[(Int, Int)]()of(row, column)tuples. - Loop over the rows with
r <- 0 until rows. - Loop over the columns inside it with
c <- 0 until cols. - Test for the edge:
onBorderis true whenris the first or last row, orcis the first or last column. - Find the border
'O'cells:if (onBorder && board(r)(c) == 'O'). - Mark the cell safe. Marking it as it joins the queue means no other cell can add it a second time.
- Enqueue
(r, c). Its neighbours are checked when it leaves the queue. - Define
directionsas the four moves: up, down, left, right. Each changes the row or the column by one, never both. - Loop while
queue.nonEmpty. - Dequeue a cell and unpack it into
val (row, col). - Loop over
directionswith(dr, dc) <- directions. - Compute the neighbour:
nextRowandnextCol. - Check the bounds before using them as indexes. Cells on the edge have neighbours outside the board.
- Test the neighbour: it must hold
'O'and not be marked yet. An'O'next to a safe one is in the same region, so it is safe too. - Mark it safe.
- Enqueue it.
- Second pass: loop
rover0 until rows. - Inner loop:
cover0 until cols. - Capture: if
board(r)(c) == 'O' && !safe(r)(c), set the cell to'X'.
The solution in Scala
class Solution {
def solve(board: Array[Array[Char]]): Unit = {
val rows = board.length
val cols = board(0).length
val safe = Array.fill(rows, cols)(false)
val queue = mutable.Queue[(Int, Int)]()
for (r <- 0 until rows) {
for (c <- 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.enqueue((r, c))
}
}
}
val directions = Array((-1, 0), (1, 0), (0, -1), (0, 1))
while (queue.nonEmpty) {
val (row, col) = queue.dequeue()
for ((dr, dc) <- 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.enqueue((nextRow, nextCol))
}
}
}
}
for (r <- 0 until rows) {
for (c <- 0 until cols) {
if (board(r)(c) == 'O' && !safe(r)(c)) {
board(r)(c) = 'X'
}
}
}
}
}
Scala details worth a look:
- Chars use single quotes.
'O'is aChar, the type of each cell."O"would be aString, and Scala 3 refuses to compare the two: the compiler reports "Values of types Char and String cannot be compared with == or !=". - Two sets of parentheses.
board(r)(c)reads the rowr, then the cellcof that row. Assignment works the same way:board(r)(c) = 'X'changes the array in place. - Tuple patterns.
val (row, col) = queue.dequeue()unpacks the tuple. The same pattern works in afor:for ((dr, dc) <- directions). Array.fill(rows, cols)(false)builds a two-dimensional array of booleans. Unlikeboard,safeis a separate array of our own, so the search can mark cells without changing the board.- No return value. A method declared with
: Unit =ends with the loops, and callers read the result from the array they passed in.
Complexity
Time: O(m * n). The first pass visits every cell once. Each cell joins the queue at most once and checks four neighbours. The last pass visits every cell once.
Space: O(m * n). The safe grid has one entry per cell, and the queue can hold a good share of the cells at once.
Edge cases and common mistakes
- A one-cell board.
OstaysO: the only cell is on every edge.XstaysX. - Keeping only the border cells. A version that turns every
'O'that is not on the edge into'X', without spreading from the border, wrecks the 5 by 5 board above: the middle region is captured even though it reaches the last row. The result keeps only the border cells (shown below the list). - Diagonal neighbours. Only four directions connect cells. The corner
'O'in the example touches the middle region only diagonally; it is safe because it is on the edge itself. - A board with no
'O'on the edge.XXX / XOX / XXX(three rows) becomes allX. - A ring of
'O'cells around an'X'.OOO / OXO / OOOstays unchanged: the ring is on the edge.
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
Cells that connect to the edge through other 'O' cells must be kept as well, which is why the search spreads from the border.
Testing
- Rows
XXXX,XOOX,XXOX,XOXXbecomeXXXX,XXXX,XXXX,XOXX. - A board holding just
OstaysO. - Rows
XXX,XOX,XXXbecome allX. - Rows
OOO,OXO,OOOstay the same.
