Problem overview
The function exist receives a grid of letters, board (a [][]byte), and a string word. It returns true when the word can be traced through the grid and false otherwise. A trace starts at any cell and moves one step at a time to the cell above, below, left or right, never diagonally. The cells visited must spell word in order, no cell can be used twice in one trace, and letters are case-sensitive.
Take this board:
A B C E
S F C S
A D E E
"ABCCED" can be traced: A at (0,0), B at (0,1), C at (0,2), C at (1,2), E at (2,2), D at (2,1). "SEE" works too, with S at (1,3), E at (2,3) and E at (2,2). "ABCB" fails: A, B, C is fine along the top row, but the only B next to that C is the one already used.
Approach: depth-first search with backtracking
The word can start in any cell, so the function tries every cell as the first letter. From a cell that matches, it asks the four neighbours to match the next letter, then theirs, and so on. A path is abandoned at its first wrong letter, which keeps the search small in practice.
Two things make this work:
- Marking cells as used. While a cell is on the current path, it is overwritten with
'#'. The board contains only letters, so a marked cell can never match a letter of the word, and deeper calls can't step on it again. - Unmarking on the way back. When a call has tried all four neighbours, it puts its letter back. A different path, tried later, may need that cell. This undo is the backtracking.
In the worst case the search starts from each of the R·C cells and branches into up to four directions for each of the L letters (three after the first, since the cell it came from is marked), so the time is O(R·C·4^L). The constraints keep the board at most 6 by 6 and the word at most 15 letters, and the early stop at wrong letters makes real inputs much cheaper than the worst case.
Step by step
The outer function
- Loop over the rows with
for row := range board. With one variable,rangeover a slice yields the indexes. - Loop over the columns of that row with
for col := range board[row]. Together the two loops visit every cell as a possible start. - Call
dfswith the board, the word, the cell and index 0, the first letter. If it returnstrue, returntrueat once: one successful start is enough. - Return
falseafter both loops. No start worked.
The helper dfs
- Write the signature. It takes the board, the word, a cell and
index, the position inwordof the letter this cell must hold. It reports whether the letters fromindexto the end can be traced starting at the cell. - Everything matched. If
index == len(word), returntrue. This test comes first, because the cell that matched the last letter has already done its job, and the next cell may even lie outside the board. - Outside the board. Return
falsewhen the row or column is below 0 or past the last row or column. Testing here means no caller has to check its neighbours. - Wrong letter. Return
falsewhenboard[row][col]differs fromword[index]. Both are bytes, so they compare directly. A cell already on the path holds'#'and fails this test too. - Mark the cell by setting it to
'#'. - Start with
found := false. The function can't return as soon as a neighbour succeeds, because the cell has to be unmarked first. - Write the four moves as data:
directionsis a[][2]intof {row change, column change} pairs. One loop then replaces four copies of the same call. - Loop over the moves. For each
d, setfound = found || dfs(...)on the neighbour atrow+d[0],col+d[1]withindex+1. - Unmark the cell by writing
word[index]back into it. - Return
found.
The solution in Go
func exist(board [][]byte, word string) bool {
for row := range board {
for col := range board[row] {
if dfs(board, word, row, col, 0) {
return true
}
}
}
return false
}
func dfs(board [][]byte, word string, row int, col int, index int) bool {
if index == len(word) {
return true
}
if row < 0 || row >= len(board) || col < 0 || col >= len(board[0]) {
return false
}
if board[row][col] != word[index] {
return false
}
board[row][col] = '#'
found := false
directions := [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}
for _, d := range directions {
found = found || dfs(board, word, row+d[0], col+d[1], index+1)
}
board[row][col] = word[index]
return found
}
Go details worth knowing:
- Bytes against bytes.
boardis a[][]byteandword[index]is abyte, so they compare without conversion.'#'is an untyped constant that becomes a byte when assigned to a cell. - The board is shared, not copied. A slice of slices passed to a function still points at the same cells, so marking a cell in a deep call is visible to every call above it. That is what makes the marks work, and it is why the unmarking step is required: nothing restores the board automatically.
[2]intis an array of exactly two ints, a fixed-size value, which suits a pair of offsets.[][2]intis a slice of such pairs.- Short-circuit
||. The right side offound || dfs(...)runs only whilefoundis stillfalse, so once a neighbour completes the word, the others are skipped. The loop still reaches the unmarking line, which is what we want. range board[row]gives the columns of that row, which also works for rows of different lengths. The bounds test useslen(board[0]), which is right here because every row has the same width.
Complexity
Time: O(R·C·4^L), where R and C are the board's rows and columns and L is the length of word. There are R·C starting cells, and each search goes at most L letters deep with at most four choices per level. The bound is loose, because marked cells and wrong letters cut paths early.
Space: O(L) for the recursion: one call per letter matched so far. The marks live in the board itself, so no visited array is needed.
Edge cases and common mistakes
- Forgetting to unmark. The search then leaves
'#'in cells that failed paths passed through, and later paths can't use them. On the board["AAAB"](one row) with the word"AAB", the correct function returnstrue, but a version without the unmarking line returnsfalse: the first attempt, from the leftmost A, marks cells and fails, and the successful path needs one of them. - Reusing a cell. Without marking, the one-row board
["AB"]would trace"ABA"by stepping back onto the first A. With marking,existreturnsfalsefor it. - Checking the bounds after reading the cell. Reading
board[row][col]before the bounds test panics with an index out of range at an edge. - Testing the end of the word too late. If the
index == len(word)test came after the bounds test, a word ending at the edge of the board would fail, since the call after the last letter steps outside. - Case.
aandAare different letters. On the example board,"A"is found and"a"isn't. - The board is modified during the search. It is restored by the time
existreturns, as long as every mark is undone, so the caller sees the original board.
Testing
On the 3 by 4 board above (ABCE, SFCS, ADEE):
"ABCCED"givestrue."SEE"givestrue."ABCB"givesfalse."SFCE"givestrue."ASA"givestrue(down the first column)."A"givestrueand"a"givesfalse.
On one-row boards: ["a"] with "a" gives true, ["a"] with "aa" gives false, and ["aa"] with "aaa" gives false.
