Word Search
Table of Contents + β
Word Search is a grid puzzle that interviewers love. You get a board of letters. You have to find a word by walking from cell to cell. The catch is you can only move up, down, left, or right, and you cannot reuse a cell. This is backtracking on a grid, and it teaches you how to mark a path and then clean it up.
π― The Problem
You get a grid of letters and a word. You must find out if the word exists in the grid as a walk.
- A walk is a chain of neighbor cells with no repeats.
- Neighbors are the cells directly up, down, left, and right.
- You cannot move diagonally.
- You cannot use the same cell twice in one path.
- For the word
ABCCED: start atA, go right toB, right toC, down toC, down toE, left toD. - Each step is a neighbor and no cell repeats, so the answer is
true.
Input:board = [ ["A","B","C","E"], ["S","F","C","S"], ["A","D","E","E"]]word = "ABCCED"Output: true
Explanation: A -> B -> C -> C -> E -> D forms the wordby moving between neighboring cells without reusing any cell.Here is the grid with the path for ABCCED shown step by step. Each arrow moves to a neighbor cell.
π’ Approach 1: DFS With a Separate Visited Grid (Brute Force)
The idea in one line: search from every cell, and track used cells in a second grid.
The idea:
- Try starting the word from every cell in the grid.
- Follow one path as far as it matches, letter by letter.
- Keep a separate boolean grid that marks which cells the current path uses.
How it works:
- Before stepping onto a cell, check the visited grid so the path never reuses it.
- Mark the cell visited, search the four neighbors, then unmark it on the way back.
Why it is weak:
- It allocates a whole extra grid the same size as the board.
- It reads and writes that grid on every step.
- The logic is fine, but the extra memory is wasted when the board itself can hold the mark.
Here is the separate-visited-grid code:
def exist(board, word): rows, cols = len(board), len(board[0])
def dfs(row, col, index, visited): if index == len(word): return True if row < 0 or col < 0 or row == rows or col == cols: return False if (row, col) in visited or board[row][col] != word[index]: return False
visited.add((row, col)) found = ( dfs(row + 1, col, index + 1, visited) or dfs(row - 1, col, index + 1, visited) or dfs(row, col + 1, index + 1, visited) or dfs(row, col - 1, index + 1, visited) ) visited.remove((row, col)) return found
return any(dfs(r, c, 0, set()) for r in range(rows) for c in range(cols))β‘ Approach 2: DFS With In-Place Mark and Restore (Best)
The idea in one line: mark a used cell right inside the board, then restore it after the neighbors.
The idea:
- DFS means depth first search, following one path as far as it goes before trying another.
- Match one letter at a time against the current letter of the word.
- Use the board cell itself to remember βthis cell is in the current pathβ.
How it works:
- If the cell is off the grid or the letter does not match, this path fails right away.
- If it matches and it was the last letter, the whole word is found.
- Save the cellβs letter, then change it to a marker like a hash sign.
- Search the four neighbors for the next letter.
- Restore the original letter after the neighbors return.
Why it is fast:
- No extra grid is needed, since the board carries the mark.
- The marker can never match a real word letter, so a path cannot step back onto itself.
- Mark, search the neighbors, then unmark. That is the whole rhythm.
Steps to Solve
- Loop over every cell in the grid as a possible start.
- From a start cell, run a depth first search that tracks which letter you are matching.
- If the cell is out of bounds, or does not match the current letter, this path fails.
- If it matches and it was the last letter, the word is found.
- Mark the current cell as used so it is not reused in this path.
- Search all four neighbors for the next letter.
- Restore the cellβs letter after the neighbors return, then move on.
Here is the DFS decision tree from the starting A. Each branch is one neighbor we try. A wrong letter ends that branch.
This Python version marks a visited cell with a hash sign and restores it after the search.
def exist(board, word): rows, cols = len(board), len(board[0])
def dfs(r, c, index): if index == len(word): # matched every letter return True if r < 0 or c < 0 or r >= rows or c >= cols: return False # walked off the grid if board[r][c] != word[index]: # letter does not match return False
saved = board[r][c] board[r][c] = "#" # mark this cell as used
found = (dfs(r + 1, c, index + 1) or # down dfs(r - 1, c, index + 1) or # up dfs(r, c + 1, index + 1) or # right dfs(r, c - 1, index + 1)) # left
board[r][c] = saved # restore (backtrack) return found
for r in range(rows): for c in range(cols): if dfs(r, c, 0): return True return False
board = [ ["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"],]print(exist(board, "ABCCED"))The output of the above code will be:
TrueLet us read the Python version line by line. The mark and restore steps are the heart of grid backtracking.
rows, cols = len(board), len(board[0]) reads the grid size once. We use these to check when a move walks off the edge.
if index == len(word): is the success base case. index is which letter of the word we are trying to match. When index reaches the word length, every letter matched in order. So the word is found and we return True.
if r < 0 or c < 0 or r >= rows or c >= cols: is the boundary check. If the row or column is outside the grid, we cannot stand there. So this path fails with False.
if board[r][c] != word[index]: checks the letter. If the current cell does not hold the letter we need, this path is wrong. We stop and return False.
saved = board[r][c] remembers the original letter. Then board[r][c] = "#" marks the cell as used. The hash sign can never match a real word letter. So no neighbor search can step back onto this cell during the same path.
The four dfs calls explore the neighbors. They try down, up, right, and left, each looking for the next letter at index + 1. The or means we stop as soon as any direction finds the word.
board[r][c] = saved is the backtrack. It puts the original letter back. So the cell is free again for any other path. Without this restore, one failed path would block cells forever.
β±οΈ Time and Space Complexity
Let the grid have m times n cells. From each cell we branch into four directions, and the path can be as long as the word, call its length L. So the search from one start is about 4 to the power of L. We try every cell as a start, so the total time is O(m Γ n Γ 4α΄Έ). The space is O(L) for the recursion depth, since the deepest path equals the word length.
| Approach | Time Complexity | Space Complexity |
|---|---|---|
| DFS with a separate visited grid | O(m Γ n Γ 4α΄Έ) | O(m Γ n + L) |
| DFS with in-place mark and restore | O(m Γ n Γ 4α΄Έ) | O(L) |
Tip
Changing the cell to a marker is a neat way to track visited cells without an extra array. Just remember to restore it after the four neighbor calls. Forgetting the restore is the most common bug here.
π§© Key Takeaways
- β Try every cell as a possible start for the word.
- β Match one letter at a time, moving only up, down, left, or right.
- β Mark the current cell as used so one path never reuses a cell.
- β Restore the cell after exploring its neighbors, so other paths stay free.
- β The success base case is when the letter index reaches the word length.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Which moves are allowed when forming the word?
Why: You can move only to the four direct neighbors: up, down, left, and right. No diagonals.
- 2
Why do we mark a cell, for example with a hash sign, during the search?
Why: Marking the cell prevents the path from stepping back onto a cell it is already using.
- 3
What is the success base case of the DFS?
Why: Reaching an index equal to the word length means every letter matched, so the word is found.
- 4
What must we do after exploring a cell's four neighbors?
Why: Restoring the original letter unmarks the cell, which is the backtracking step that frees it for other paths.