Problem overview
The function solve receives board, a [][]byte grid in which every cell is 'X' or 'O'. 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 connected 'O' cells. A region is surrounded when none of its cells lies on the edge of the board, that is, in the first or last row or the first or last column. The function 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 the first example, the board is
X X X X
X O O X
X X O X
X O X X
The three 'O' cells in the middle form one region, and none is on the edge, so they are captured. The 'O' in the last row is on the edge and stays:
X X X X
X X X X
X X X X
X O X X
A board with a single 'O' keeps it, because that cell is on every edge.
Approach: start from the edge
The direct approach asks, for each 'O', whether some path of 'O' cells leads to the edge. That can repeat a lot of work: one large region in the middle of the board gets searched again from each of its cells, which can cost about (m · n)² in total.
The lesson turns the question around. Every 'O' on the edge is safe, and so is every 'O' connected to a safe one. So a breadth-first search (BFS) starts from all the edge 'O' cells at once and marks every cell it reaches as safe. Afterwards, any 'O' that is not marked is surrounded, and one more pass over the board turns it into 'X'.
The search uses a queue instead of recursion. A single region can wind through the whole board, and a recursive search that goes tens of thousands of calls deep is a risk. A queue also needs no extra function.
The third example shows why the search must follow only the four directions. The board is
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
The middle region winds down to the 'O' in the last row, so none of it is captured. The 'O' in the top-left corner touches that region only diagonally, but it is on the edge itself. The 'O' at row 3, column 1 is closed in on all four sides and becomes 'X'. The board does not change, apart from that one cell.
Step by step
- Read the size.
rowsislen(board)andcolsislen(board[0]), since all rows have the same length. - Create
safewithmake([][]bool, rows). It marks the cells connected to the edge. This only makes the outer slice, and each row is made in the loop. - Create the queue as
[][2]int{}, a slice of row and column pairs. It holds the safe cells whose neighbours are still to be checked. - Loop over the rows and, inside it, create
safe[r]asmake([]bool, cols), which starts as allfalse. - Loop over the columns. For each cell, compute
onBorder: row 0, last row, column 0 or last column, joined with||. - Seed the search. For a cell with
onBorder && board[r][c] == 'O', setsafe[r][c]totrueand append[2]int{r, c}to the queue. Marking the cell as it joins the queue keeps it from being added twice. - Define
directions: up, down, left and right as {row change, column change} pairs. - Run the BFS with
for len(queue) > 0. Take the front cell and drop it withqueue = queue[1:], then unpackrow, col := cell[0], cell[1]. - Check each neighbour. Compute
nextRowandnextCol, and make sure they are inside the board before indexing with them. - Spread to
'O'neighbours that are not yet marked: set them safe and append them to the queue. - Capture. A second pass over the board turns every
'O'that is not safe into'X'.
The solution in Go
func solve(board [][]byte) {
rows := len(board)
cols := len(board[0])
safe := make([][]bool, rows)
queue := [][2]int{}
for r := 0; r < rows; r++ {
safe[r] = make([]bool, cols)
for c := 0; c < cols; c++ {
onBorder := r == 0 || r == rows - 1 || c == 0 || c == cols - 1
if onBorder && board[r][c] == 'O' {
safe[r][c] = true
queue = append(queue, [2]int{r, c})
}
}
}
directions := [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
for len(queue) > 0 {
cell := queue[0]
queue = queue[1:]
row, col := cell[0], cell[1]
for _, d := range directions {
nextRow := row + d[0]
nextCol := col + d[1]
if nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols {
if board[nextRow][nextCol] == 'O' && !safe[nextRow][nextCol] {
safe[nextRow][nextCol] = true
queue = append(queue, [2]int{nextRow, nextCol})
}
}
}
}
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if board[r][c] == 'O' && !safe[r][c] {
board[r][c] = 'X'
}
}
}
}
The Go details worth a closer look:
- Bytes and quotes. Each cell is a
byte, and'O'in single quotes is the letter's code, soboard[r][c] == 'O'compares directly. Double quotes would make a string, which can't be compared with a byte. - A slice of slices.
make([][]bool, rows)createsrowsnil rows. Each one needs its ownmake, which the first loop does before any cell of that row is read. - Pairs as arrays.
[2]intis an array of exactly two integers. Unlike a slice, it is a value, socellis a copy of the pair in the queue. - The queue is a slice.
appendadds at the back andqueue[1:]drops the front without copying the rest. - Changing the board in place. A slice header is copied when passed to a function, but it points at the same rows, so writing
board[r][c] = 'X'changes the caller's grid. That is why the function needs no return value. - The bounds check comes first. Indexing
board[nextRow][nextCol]with a negative value or one past the end panics, so the position is tested before it is used.
Complexity
Time: O(m · n). The first pass visits each cell once. Each cell joins the queue at most once, because it is marked as it joins, and each cell taken from the queue 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 holds at most every cell once.
Edge cases and common mistakes
- Single cell. A board holding just
OstaysO. A board with justXhas nothing to change. - Every cell on the edge. Any board with fewer than three rows or three columns has no cell off the edge, so it never changes.
O X O Ostays the same. - All
'O'. A 3 × 3 board ofOcells keeps every cell, since the middle one is connected to the edge. - A lone inner
'O'. In a 3 × 3 board ofXcells with anOin the middle, the middle becomesX. - Looking only at the edge cells. Capturing every interior
'O'without spreading from the edge is wrong. On the third example it turns the winding region intoXand leaves only the two cells 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 - Counting diagonals. A search that also steps diagonally connects the region in the first example to the edge
'O'in the last row, so nothing is captured there. The expected result needs the four directions only. - Large regions. A 200 × 200 board whose inner 198 × 198 block is all
Ois captured entirely, and if one edge cell is anOthat touches the block, all 39,205Ocells stay. The queue handles this without recursion.
Testing
- The first example board becomes all
Xexcept theOat row 3, column 1. - A board with one
Okeeps it. - The third example changes only the
Oat row 3, column 1. O X O O(one row) is unchanged.- A 3 × 3 board with
Oin the middle andXelsewhere becomes allX.
