Problem overview
The function exist(board, word) receives a grid of letters, a list of rows where each row is a list of one-letter strings, and a string word. It returns True when the word can be traced through the grid and False otherwise.
Tracing means starting at any cell and moving up, down, left or right, one cell per letter, so that the cells spell the word in order. Diagonal moves are not allowed, each cell can be used at most once, and letters are case-sensitive.
Take this grid:
A B C E
S F C S
A D E E
The word "ABCCED" is found: A at row 0, column 0, then B, C along the top row, down to the C in row 1, down to the E in row 2, and left to D. "SEE" is also found, going from the S at the end of row 1 down to the E below it and left to the next E. "ABCB" is not: the path A, B, C reaches the C in row 0, but the only B next to it is the one already used.
Approach: depth-first search with backtracking
The word can start anywhere, so the function tries every cell as the first letter. From a starting cell, the search is a depth-first search: match one letter, then ask the four neighbours to match the rest of the word. Whenever a cell holds the wrong letter, that path stops at once, which keeps the search small in practice.
The rule that each cell is used once needs bookkeeping. While a cell is part of the current path, no deeper call may step on it again. The code marks the cell by overwriting its letter with '#', which is never a letter, so the normal "does this cell hold the right letter?" test also rejects cells already on the path. When the search comes back out of the cell, it restores the letter. That undo is the "back" in backtracking: a different path, tried later, may use this cell again.
In the worst case each cell starts a search that branches into up to 4 directions for each letter of the word (3 after the first, since the cell it came from is marked). The time is O(R * C * 4^L), where R and C are the grid's dimensions and L is the length of the word. In practice most branches stop at the first wrong letter.
Step by step
The outer function
- Loop over the rows, with
rowinrange(len(board)). - Loop over the columns inside it, with
colinrange(len(board[0])). Every row has the same length, so the first row tells the width. - Try the cell as the start. Call
self.dfs(board, word, row, col, 0), where 0 is the index of the first letter. If it returnsTrue, returnTruestraight away: one working start is enough. - Return
Falseafter both loops. Reaching that line means no cell could start the word.
The helper dfs
- Declare it with the board, the word, a cell and
index, the position inwordof the letter this cell must hold. It answers whether the letters fromindexto the end can be traced starting here. - If
indexequalslen(word), returnTrue. The previous cell matched the last letter, so the word is complete. This test comes first because the current cell may be off the board, and then the later tests would be wrong to reject it. - Return
Falseif the cell is outside the board. Neighbours of edge cells land outside, and checking here spares every caller from doing it. - Return
Falseif the cell's letter is notword[index]. This also rejects cells marked'#'. - 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 must be unmarked first. - List the four moves as
[row change, column change]pairs. One loop over this data replaces four copies of the same call. - Loop over the pairs, unpacking each into
dranddc. - Update
foundwithfound or self.dfs(...)on the neighbour, withindex + 1for the next letter. Becauseorevaluates its right side only whilefoundis stillFalse, the remaining directions are skipped after the first success. - Restore the cell: put
word[index]back intoboard[row][col]. - Return
found.
The solution in Python
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
for row in range(len(board)):
for col in range(len(board[0])):
if self.dfs(board, word, row, col, 0):
return True
return False
def dfs(self, board, word, row, col, index):
if index == len(word):
return True
if row < 0 or row >= len(board) or col < 0 or col >= len(board[0]):
return False
if board[row][col] != word[index]:
return False
board[row][col] = '#'
found = False
directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]
for dr, dc in directions:
found = found or self.dfs(board, word, row + dr, col + dc, index + 1)
board[row][col] = word[index]
return found
Some Python details are worth noting.
- The board is changed in place. Lists are mutable and passed by reference, so every call of
dfssees the same grid. That is what makes the'#'mark visible to deeper calls. Because every mark is undone beforedfsreturns, the board is the same afterexistfinishes as before it started. - Negative indexes wrap around in Python.
board[-1]is the last row, not an error. That is why the bounds test must come before any read ofboard[row][col]: without it, a move off the top or left edge would silently read the opposite edge. for dr, dc in directionsunpacks each two-element list into two names.found or self.dfs(...)returns the first truthy operand, and both operands here are booleans, sofoundstays a properTrueorFalse.
Complexity
The time is O(R * C * 4^L) in the worst case, from the branching described above, with L at most 15 and a grid of at most 6 by 6 here. The extra space is O(L): the recursion goes at most one call per letter of the word, and the marks are made inside the existing grid, so no visited set is needed.
Edge cases and common mistakes
- Forgetting to unmark. If the cell keeps its
'#'after a failed branch, later paths can no longer pass through it, and the function can miss words that exist. - Checking the end of the word too late. If the "word complete" test comes after the bounds test, a word whose last letter sits on an edge cell would be rejected, because its final neighbour call lands off the board.
- Reusing a cell. The word
"ABCB"on the grid above isFalsefor exactly this reason. Without the marks it would beTrue. - Case.
"abc"is not found on a grid of capital letters. - A one-letter word needs only one matching cell. A single-cell grid holding
"a"answersTruefor"a"andFalsefor"aa", since the one cell can't be used twice. - Diagonals. Only the four directions in the list are allowed.
Testing
On the grid above, the results are:
| word | result |
|---|---|
"ABCCED" | True |
"SEE" | True |
"ABCB" | False |
"A" | True |
"ABCESEEDAS" | True |
"abc" | False |
Also try [["a"]] with "a" (True) and with "aa" (False), and [["a", "a"]] with "aaa" (False: two cells can't spell three letters).
