Problem overview
The method exist gets board, an Array[Array[Char]] grid of letters, and a word. It returns true when the word can be traced through the grid. A trace starts at any cell and moves one step at a time to a cell directly above, below, left or right of the current one, never diagonally. The cells visited must spell the word in order, and no cell may be used twice. Letters are case-sensitive.
Take this board:
A B C E
S F C S
A D E E
"ABCCED" is found: A at row 0, column 0, then B, then C at (0,2), the C below it at (1,2), the E below that at (2,2) and finally the D to its left. "SEE" is found by S at (1,3), E at (2,3), E at (2,2). "ABCB" is not found: after A, B, C the only B next to the C is the one already used.
Approach: depth-first search with undo
A word can begin in any cell, so the method tries every cell as the first letter. From a start, it needs to explore paths, and a path can branch four ways at each step. This is backtracking: extend the path one letter at a time, and when a branch fails, undo the last step and try the next option.
One recursive call does one job: dfs(board, word, row, col, index) answers whether the letters of word from position index to the end can be traced starting at cell (row, col). It stops at the first wrong letter, so most paths die after a step or two.
The rule that a cell can't be reused needs a record of the current path. Instead of a separate table, the search overwrites the cell with '#' while it is on the path. The board holds only letters, so '#' never matches a letter of the word, and a deeper call that steps on this cell fails the letter test by itself. When the call finishes, it writes the original letter back, so later paths can use the cell again.
In the worst case, the search from each of the R·C cells branches four ways for the first letter and at most three ways for each later letter, because one neighbour is the cell it came from. With a word of length L this is O(R·C·4^L) time. The recursion is at most L calls deep, so the extra space is O(L).
Step by step
exist
- Loop over the rows with
for (row <- board.indices).indicesis the range of valid positions, from 0 to the last. - Loop over the columns with
board(0).indices. Together the two loops visit every cell as a possible start. - Call
dfswith the start cell and index 0, the first letter. If it returnstrue, returntrueat once: one successful start is enough. - Return
falseafter both loops. No start worked.
dfs
- Declare the helper with the board, the word, the cell and the index of the letter this cell must hold, returning
Boolean. - All letters matched: if
index == word.length, returntrue. This test comes first on purpose. The previous call matched the last letter, so the current cell is irrelevant, and it may even lie outside the board. - Outside the board: return
falseif the row or column is below 0 or past the last one. Edge cells have neighbours outside the grid, and checking here saves every caller from doing it. - Wrong letter: return
falseifboard(row)(col)differs fromword(index). A cell on the current path holds'#', so it fails here too. - Mark the cell by setting it to
'#'. - Prepare the result: a
var foundstarting atfalse. The method can't return as soon as a neighbour succeeds, because the cell must be restored first. - Write the four moves as data:
directionsis an array of(row change, column change)pairs for up, down, left and right. - Loop over the moves and update
foundwithfound || dfs(...)for the neighbour atrow + d(0),col + d(1), looking for the letter atindex + 1. The||skips the call oncefoundis alreadytrue. - Unmark the cell by putting
word(index)back. This is the backtracking step. - Return
found.
The solution in Scala
class Solution {
def exist(board: Array[Array[Char]], word: String): Boolean = {
for (row <- board.indices) {
for (col <- board(0).indices) {
if (dfs(board, word, row, col, 0)) {
return true
}
}
}
return false
}
def dfs(board: Array[Array[Char]], word: String, row: Int, col: Int, index: Int): Boolean = {
if (index == word.length) {
return true
}
if (row < 0 || row >= board.length || col < 0 || col >= board(0).length) {
return false
}
if (board(row)(col) != word(index)) {
return false
}
board(row)(col) = '#'
var found = false
val directions = Array(Array(-1, 0), Array(1, 0), Array(0, -1), Array(0, 1))
for (d <- directions) {
found = found || dfs(board, word, row + d(0), col + d(1), index + 1)
}
board(row)(col) = word(index)
return found
}
}
The Scala details worth noticing:
- Arrays are mutable and indexed with parentheses.
board(row)(col)reads a cell, andboard(row)(col) = '#'writes it. The board is shared by every recursive call, so the mark is visible to all of them. That is the point, and it is also why the undo matters: without it the board would stay damaged for the next start. Charcomparisons.word(index)is aChar, and'#'is aCharliteral. Comparing twoCharvalues with!=compares the characters, and uppercase and lowercase letters differ.for (d <- directions)walks an array whose elements are themselves arrays of twoInt, sod(0)andd(1)are the row and column changes.returninside aforloop. The loop body is a function under the hood. Scala 3.5 still acceptsreturnthere and the code works, but the compiler prints a warning that non-local returns are no longer supported. The message points toboundaryandboundary.breakinscala.utilas the replacement.- Short-circuit
||. The right side is evaluated only when the left side isfalse, so after one neighbour finishes the word, the remaining neighbours aren't searched.
Complexity
Time: O(R·C·4^L) in the worst case, with R rows, C columns and a word of L letters. Each of the R·C cells starts a search, and a search goes at most L letters deep with at most four choices at each. The worst case needs a board and a word that keep matching for many letters, such as a grid of a single repeated letter and a word that ends with a different one. Ordinary boards stop much earlier.
Space: O(L). The marks live inside the board, so the only extra memory is the call stack, with one call per letter matched so far.
Edge cases and common mistakes
- Reusing a cell. On a one-row board
a a, the word"aaa"is not found: it needs three cells and the board has two. Without the'#'mark, the search would step back and forth between the same two cells and answertrue. - Forgetting to unmark. The cell stays
'#'after a failed branch, and later paths that need that letter fail. A correct run leaves the board exactly as it was, whetherexistreturnstrueorfalse. - Checking the index after the bounds. If the
index == word.lengthtest came after the bounds test, a word ending at a corner would fail, because the neighbour after its last letter is outside the board. - Case sensitivity. On a board holding
aandb, the word"A"returnsfalse. - Single cell. A 1x1 board with
afinds"a"and not"aa". - Diagonal moves. Only the four directions are in the array, so the search never steps diagonally.
- Returning when a neighbour succeeds. Returning inside the loop would skip the restore step. Collecting the answer in
foundand returning at the end keeps the board intact.
Testing
On the board above:
"ABCCED"givestrue."SEE"givestrue."ABCB"givesfalse."ABCESEEEFS"givesfalse."ASAD"givestrue: A (0,0), S (1,0), A (2,0), D (2,1)."a"givesfalse, because the board has only uppercase letters.
On other boards: a alone with "a" gives true and with "aa" gives false; a a with "aaa" gives false; a b with "ba" gives true.
