Word search with a trie
In one line
To find many words on a letter grid at once, put the words in a trie and run one backtracking DFS per cell that follows the trie, so every path stops the moment no word can continue it.
What it is
The classic problem is Word Search II (LeetCode 212): given a grid of letters and a list of words, return every word that can be formed by moving between horizontally or vertically adjacent cells, without using the same cell twice in one word.
Searching each word separately repeats the same work: 'oath', 'oat' and 'oats' all explore the same o-a-t paths. A trie merges the words, so one DFS from each cell explores the grid and the trie together. If the letters walked so far are not a prefix of any word, the trie has no child for the next letter and the DFS backs off immediately. That pruning is what makes the problem fast enough.
Why interviewers ask
This is a favourite hard question because it combines three skills: building a trie, grid backtracking with a visited mark, and pruning. Interviewers look for whether you see that searching words one by one is wasteful, whether you avoid duplicate results, and whether you can explain the extra optimisation of trimming finished branches from the trie.
How it works
- 1Insert all words into a trie. At the end node of each word, store the word itself so you can record it without rebuilding the string.
- 2For every cell (r, c), start a DFS with the root as the parent node.
- 3In the DFS, look up the cell's letter among the parent's children. If there is no child, return: no word continues this way.
- 4If the child node holds a word, add it to the results and remove it from the node so it is not reported twice.
- 5Mark the cell as visited (for example, replace the letter with '#'), recurse into the four neighbours with the child node, then restore the letter. This is the backtracking step.
- 6After returning, if the child node has no children and no word left, delete it from its parent. Later searches then stop even earlier, and once all words are found the trie shrinks to nothing.
Worked example · Board 4x4, words oath, pea, eat, rain
- Board rows: o a a n / e t a e / i h k r / i f l v. Trie roots: o (oath), p (pea), e (eat), r (rain).
- Cell (0,0) 'o': root has 'o', go on. Neighbours: (1,0) 'e' is not a child of 'o', pruned; (0,1) 'a' is, go to it.
- From (0,1) 'a' the trie wants 't'. Neighbours (0,2) 'a' and (1,1) 't': only 't' matches. From (1,1) 't' the trie wants 'h': neighbour (2,1) is 'h'. That node holds 'oath': record it.
- Cell (0,1) 'a', (0,2) 'a', (0,3) 'n': root has no 'a' or 'n' child, each returns in one step.
- Cell (1,0) 'e': root has 'e', trie wants 'a' next, but neighbours are o, i, t: no 'a', pruned.
- Cell (1,3) 'e': trie wants 'a': neighbour (1,2) is 'a'; then wants 't': neighbour (1,1) is 't'. Record 'eat'.
- Cell (2,3) 'r': trie wants 'a', but neighbours are e, v, k: pruned, so 'rain' is not on the board. No cell has 'p', so 'pea' is never started.
- Result: [oath, eat]. Most cells cost a single dictionary lookup because the trie rejected them immediately.
Picture it
It is like hunting for several friends in a maze with a single map that lists all their routes at once. At every junction you only take turns that appear on at least one friend's route, and you cross a route off the map once you have found that friend.
Reference code
def find_words(board, words):
root = {}
for w in words: # build the trie; '$' holds a full word
node = root
for ch in w:
node = node.setdefault(ch, {})
node['$'] = w
rows, cols, found = len(board), len(board[0]), []
def dfs(r, c, parent):
ch = board[r][c]
node = parent.get(ch)
if node is None:
return # no word continues this way: prune
if '$' in node:
found.append(node.pop('$')) # record once, never again
board[r][c] = '#' # mark visited for this path
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
dfs(nr, nc, node)
board[r][c] = ch # undo the mark (backtrack)
if not node:
parent.pop(ch) # branch has no words left: trim it
for r in range(rows):
for c in range(cols):
dfs(r, c, root)
return foundThe trie is nested dicts with '$' holding a finished word. dfs carries the parent trie node, prunes when the board letter is not a child, records and removes words once found, marks the cell with '#' while exploring and restores it afterwards, and trims trie branches that become empty.
Key patterns
Trie-guided grid backtracking
When: Many words to find on a grid (LeetCode 212)
How: DFS from each cell carrying the current trie node; prune when the next letter is not a child.
Single word on a grid
When: Only one word to find (LeetCode 79 Word Search)
How: No trie needed: plain backtracking comparing board letters with word[i].
Word dictionary with wildcards
When: search('b.d') where '.' matches any letter (LeetCode 211)
How: Walk the trie; on '.', try every child recursively; on a letter, follow one child.
Word break with a trie
When: Split a string into dictionary words (LeetCode 139 / 140)
How: From each start index walk the trie forward along the string; every end node reached marks a possible split point, combined with DP or memoised recursion.
Real-world use
Word game solvers
Boggle and Scrabble solvers walk the board and a dictionary trie (or a DAWG / GADDAG, compressed variants) together, so they explore only letter paths that can still become real words.
Multi-pattern text matching
The Aho-Corasick algorithm is a trie of patterns plus failure links. Intrusion detection systems such as Snort and Suricata use it to scan network traffic for thousands of signatures in one pass.
Content filters
Chat moderation and spam filters load a list of banned terms into a trie or Aho-Corasick automaton and scan each message once, instead of running one search per banned term.
Genome analysis
Bioinformatics tools match many short DNA probes against long sequences using trie-based indexes over the probe set, the same 'share prefixes across many patterns' idea.
In a project
You are building a Boggle-style mobile word game and need a hint button that lists every valid word on a 5x5 board from a 100,000-word dictionary, in under 100 ms on a phone.
- Build the dictionary trie once when the app starts (or ship it pre-built), keeping only words of length 3 to 16.
- When a board is generated, run the trie-guided DFS from each of the 25 cells with an in-place visited mark.
- Remove found words from the trie copy for this board and trim empty branches to speed up later cells.
- Use the found-word list to score the board and reject boards with too few words before showing them.
Impact: Searching 100,000 words one by one would be millions of DFS starts; the trie-guided search touches only real prefixes and finishes in milliseconds, so hints are instant and every generated board is guaranteed to be playable.
Trade-offs
Use it when
- You must find many words on the same grid
- Words share prefixes, so one walk can serve several of them
- You want to stop exploring paths that cannot lead to any word
Avoid it when
- There is only one word to find: plain backtracking is simpler
- The grid is huge and the words are few and long: consider searching from the rarest letter of each word instead
- Cells can be reused in a word: then it is a different (graph or DP) problem with no visited mark
Cost: Worst case O(R x C x 4 x 3^(L-1)) for an R x C board and maximum word length L (4 directions for the first step, at most 3 afterwards because you cannot go back). Building the trie is O(total characters in the words). Extra space O(total characters) for the trie plus O(L) recursion depth.
Common mistakes
- Searching each word with its own DFS, which repeats the shared-prefix work and times out.
- Reporting the same word twice when it appears along two paths; remove the word from its node or use a set.
- Forgetting to restore the cell after the recursive calls, which corrupts the board for later starts.
- Returning as soon as a word is found, which misses longer words that continue the same path ('oat' and 'oath').
- Using a separate visited set but forgetting to remove the cell from it when backtracking.
Interview questions
Quick revision
- Many words on one grid = trie + DFS from every cell.
- Carry the current trie node in the DFS; missing child = prune.
- Store the word at its end node; pop it when found (no duplicates).
- Mark visited in place ('#'), recurse 4 ways, then restore.
- Do not stop at the first word: longer words may continue the path.
- Trim empty trie branches after backtracking to speed up later starts.
- Worst case O(R x C x 4 x 3^(L-1)); trie build O(total chars).
Check yourself
1. In the trie-guided DFS, when do you stop exploring from a cell?
2. The words are 'oat' and 'oath' and both are on the board. What goes wrong if you return immediately after recording 'oat'?
3. Why does the code remove the word from the trie node after finding it?
4. Why is the branching factor 3 after the first step instead of 4?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.