Word Search asks you to trace a word through a grid of letters, stepping only between side-by-side cells and never reusing a cell. It is the classic introduction to DFS with backtracking on a 2D board — the move that powers mazes, flood fills, and constraint puzzles.
Problem. Given an m x n grid of characters board and a string word, return True if word
can be spelled by a path of adjacent cells (up / down / left / right). The same cell may not be
used more than once in a single path.
Example: for the board below and word = "ABCCED", the answer is True.
A B C E
S F C S
A D E EThe slow way first
You might try to enumerate every possible path through the grid and check each one against the word. But the number of paths explodes combinatorially — there is no clean way to list them all without walking the grid anyway. The real question is: standing on a cell that matches the next letter, where can I go? That nudge — explore one neighbor as far as it goes, and rewind if it fails — is exactly depth-first search with backtracking.
The idea: walk, mark, and rewind
Start a DFS from every cell. At each cell, check whether it matches the letter we still need (word[i]). If it does, mark it visited so the path cannot loop back onto it, then recurse into its four neighbors looking for word[i+1]. If a branch dead-ends, unmark the cell (backtrack) so a different path is free to use it later. The moment i reaches the end of the word, we have spelled the whole thing.
The mark/unmark pair is the heart of backtracking: a cell is off-limits only while it sits on the current path.
Walk through it
Step through the animation. The dfs pointer walks from A at the top-left, matching A → B → C. From that first C it first tries the neighbor E, which is not the C we need — a dead-end, so it unmarks that cell and steps back. It then takes the other C going down, reaches E, and finally D, completing "ABCCED". That one backtrack is the whole lesson.
Pseudocode
dfs(r, c, i):
if i == length of word: # matched every letter
return True
if out of bounds, or board[r][c] != word[i]:
return False # this branch fails
remember board[r][c], then mark it visited
try dfs into all 4 neighbors with i + 1
restore board[r][c] # backtrack: unmark
return whether any neighbor succeeded
for each starting cell (r, c):
if dfs(r, c, 0): return True
return FalseThe Python solution
def exist(board, word):
rows, cols = len(board), len(board[0])
def dfs(r, c, i):
if i == len(word):
return True
if (r < 0 or r >= rows or c < 0 or c >= cols
or board[r][c] != word[i]):
return False
tmp = board[r][c]
board[r][c] = "#"
found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1)
or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1))
board[r][c] = tmp
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return Falsedfs(r, c, i)asks: can I spellword[i:]starting at cell(r, c)?- The first
ifis the success base case — onceiequals the word length, every letter is placed. - The second
ifis the failure base case — off the board, or the letter does not match what we need. board[r][c] = "#"is the mark: a sentinel that can never equal a real letter, so the path cannot reuse this cell.- Line 13,
board[r][c] = tmp, is the backtrack — it restores the letter so other paths are free to step here. - The outer loops try the search from every cell, since the word could start anywhere.
Complexity
| Case | Time | Notes |
|---|---|---|
| Time | O(m·n·4^L) (moderate) | L = word length; 4 branches per step from each start |
| Best case | O(m·n) (moderate) | letters fail to match almost immediately |
O(L) (moderate)The branching factor is 4 (down to 3 after the first step, since we never go back the way we came), and the depth is the word length L, so a single search is O(4^L). We launch it from up to m·n cells. The extra space is just the recursion stack, at most L deep.
When this pattern shows up
Whenever a grid problem asks you to trace a path, fill a region, or place items without conflict, reach for DFS plus backtracking: match, mark, recurse, unmark. Number of Islands, maze solving, Sudoku, and N-Queens are all the same skeleton — the only thing that changes is the match test and what counts as a neighbor.
Do not forget to unmark the cell on the way out. If you mark but never restore, a cell that failed one path stays blocked forever, and a perfectly valid path that needs it later will wrongly report failure.
Practice
From the first C at (0,2) we need another C. A neighbor is E at (0,3). What happens, and where does the search go next?
1. Why do we mark a cell before recursing into its neighbors?
2. What does the line board[r][c] = tmp accomplish?
3. When does dfs return True immediately?
4. Why does the solution start a DFS from every cell?