Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Don't run Word Search I once per word. Build a trie of ALL the words, then DFS the grid a single time, advancing through the trie as you step between cells. A cell only continues a branch if its letter is a child in the trie — the trie prunes dead paths instantly. When you reach a node with a word stored, collect it.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you spot that running Word Search I per word is wastefully redundant, and can fuse the whole word list into one trie so a single grid DFS is pruned by what's still reachable. It's about combining two patterns (grid DFS + trie) under a complexity constraint.
Where candidates slip
Calling the single-word search W times (O(W · m · n · 4^L)), or forgetting to restore the visited mark on backtrack, or returning duplicates because terminal markers aren't cleared.
How to stand out
Store the word at the terminal node (no path rebuild), dedupe by nulling the marker on collection, and prune empty trie leaves so the DFS shrinks as words are found — plus reason about why the trie's shared prefixes kill the redundant work.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachExplore the full treeCollapse the tree
How to read it:Words ["oath","oat","eat"] share the o→a→t prefix, stored once. The green path is what the grid DFS follows; a board letter with no matching child is pruned instantly. Double-circle nodes carry a stored word.
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Word Search I Per Word (Naive Baseline — too slow)
1Hint 1Explore hintHide hint
You already know how to find ONE word on a board (Word Search I): DFS from each cell, matching letters.
2Hint 2Explore hintHide hint
The obvious extension is to call that search once for each word in the list.
3Hint 3Explore hintHide hint
Now count the cost: W separate full-board DFS runs, each up to O(m·n·4^L). The shared prefixes between words are re-explored every time — that's the waste.
A · Core idea
The most direct idea: we already know how to find a single word on the board (Word Search I), so loop over the word list and run that search for each one.
Why it's the natural first attempt. It reuses a known sub-routine and is obviously correct — find word 1, find word 2, and so on. In an interview it's worth stating to show you can decompose the problem.
Why it's hopelessly slow. Each call scans the whole board and DFS-branches up to 4^L. Repeating that for W = 3×10⁴ words is O(W · m · n · 4^L). Worse, the redundancy is structural: if "oath", "oat", and "oar" are all in the list, each search re-walks the o→a prefix on the board independently. The board work for shared prefixes is duplicated W-fold.
Why the trie fixes exactly this. A trie merges all the words by prefix, so the grid is walked once and the shared o→a is explored a single time. The board DFS is driven by 'what letters can still continue some word', which is precisely the trie's children set. That's the optimisation: collapse W searches into one prefix-guided traversal.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
words = ["oat","oath","oar"] on a boardsearch "oat"
DFS whole board, walk o→a→t
explores o→a onceCorrect results, but O(W · m · n · 4^L) — the prefix o→a walked W timesC · Code with teaching comments
Reference implementation previewclass Solution { public List<String> findWords(char[][] board, String[] words) { List<String> result = new ArrayList<>(); for (String word : words) { if (exists(board, word)) result.add(word);Explore the full codeHide the full code
D · Why it works and complexityO(W · m · n · 4^L) timeO(L) recursion spaceOpen the invariant and proofExplore the proofHide the proof
The naive cost is dominated by re-exploring shared prefixes once per word. Anytime you find yourself running the same prefix-walk repeatedly across many patterns, a trie is the structure that lets you do that shared work exactly once.
ComplexityO(W · m · n · 4^L) time, O(L) recursion space — For each of W words we run a full Word Search I: every one of m·n cells can start a DFS that branches into ~4 directions up to L deep → O(m·n·4^L) per word, times W words. With W up to 3×10⁴ this is far too slow.
E · Edge cases and pitfalls4Many words sharing the prefix "oa" — Each word re-explores the o→a board path from scratch; correctness holds but the redundancy is exactly what the trie eliminates.Explore edge casesHide edge cases
Edge cases handled
Many words sharing the prefix "oa"Each word re-explores the o→a board path from scratch; correctness holds but the redundancy is exactly what the trie eliminates.
Pitfalls
- Assuming reusing Word Search I is fine — the W multiplier on an exponential DFS is the trap.
- Forgetting to restore board[r][c] after the DFS, corrupting the grid for the next word's search.
- Not early-exiting on the first successful start cell, doing even more redundant work.
Approach 2 of 2 · Optimal
Trie + Single Grid DFS with Pruning (Optimal)
1Hint 1Explore hintHide hint
Instead of searching per word, what if a single grid DFS knew which letters could still extend SOME word? A trie of all the words answers that.
2Hint 2Explore hintHide hint
Build a trie of every word, storing the full word string at each terminal node. Then DFS from each cell, advancing a trie pointer alongside the grid position.
3Hint 3Explore hintHide hint
At a cell, if its letter isn't a child of the current trie node, stop — that branch can't spell any word. When you land on a node holding a word, collect it and null the marker to dedupe. Optionally prune empty leaves.
A · Core idea
Two patterns fused: a trie of the dictionary and a single backtracking DFS over the grid, with the trie acting as a pruning oracle.
Why store the words in a trie first. The naive solution wastes time re-walking shared prefixes once per word. A trie merges all words by prefix, so "oath", "oat", "oar" share one o→a path. Now the grid DFS can ask a single, cheap question at each step — 'is this board letter a child of my current trie node?' — and that question simultaneously checks all words that could continue here. We store the full word string on the terminal node so that when we arrive, we can collect it without reconstructing the path.
Why the trie is a pruning oracle. In the naive DFS we'd branch in all 4 directions and only discover failure after matching L characters. Here, the instant a neighbouring cell's letter isn't in node.children, we stop — no word starts with that prefix, so the whole subtree is dead. This collapses the exponential 4^L blow-up: we only descend where the trie says a word is still possible. That single if ch not in node.children: return is the heart of the speed-up.
Why we dedupe by nulling the terminal marker. A board may spell the same word via different paths, and we want each word once. When we collect a word at a terminal node, we clear its stored string (node.word = null). The word is now in the result, and future arrivals at that node won't re-add it — simpler and safer than a separate result-set membership check.
Why backtracking still applies on the grid. A word's path can't reuse a cell, so before recursing into neighbours we mark the current cell visited (overwrite with a sentinel like '#'), and we restore it afterwards. Skipping the restore would corrupt the board for every other start cell. Optional leaf pruning: once a trie node has no children left (all its words found and removed), we can detach it from its parent so subsequent DFS stops even earlier — the trie literally shrinks as words are discovered.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
words = ["oath","eat"], starting DFS near the 'o' and 'e' cellsInteractive diagram
The pruning decision at each cell
start (0,0)='o'Each grid step asks the trie one question. A miss kills the branch (the optimisation); a terminal node collects a word and clears its marker.
["oath", "eat"]C · Code with teaching comments
D · Why it works and complexityO(W·L) to build trie + O(m·n·4·3^(L-1)) to search timeO(W·L) trie + O(L) recursion spaceOpen the invariant and proofExplore the proofHide the proof
The mental flip is from 'for each word, search the grid' to 'search the grid once; let the trie decide which words are still alive'. The trie turns multi-pattern search into single-pass pruning: shared prefixes are explored once, impossible branches die immediately, and found words are removed so the search space only shrinks. This grid-DFS-pruned-by-a-trie is the canonical hard-trie pattern.
ComplexityO(W·L) to build trie + O(m·n·4·3^(L-1)) to search time, O(W·L) trie + O(L) recursion space — Building the trie costs O(W·L) over all words. The grid DFS visits each of m·n start cells; from there the path length is bounded by the longest word L, with the first step having 4 directions and each subsequent step ~3 (can't revisit the immediate previous cell) → O(m·n·4·3^(L-1)). Crucially the trie prunes any branch whose letters don't continue a word, so in practice the explored tree is tiny compared to the naive 4^L. Space is the trie O(W·L) plus O(L) recursion depth.
E · Edge cases and pitfalls5Two board paths spell the same word — First arrival collects it and nulls next.word; the second arrival sees word == null and adds nothing → no duplicates.Explore edge casesHide edge cases
Edge cases handled
Two board paths spell the same wordFirst arrival collects it and nulls next.word; the second arrival sees word == null and adds nothing → no duplicates.A word that is a prefix of another, e.g. "oat" and "oath"Landing on the 'oat' node collects "oat" but the DFS continues to 'h' for "oath" — both are found because collection doesn't stop the descent.
Pitfalls
- Not restoring board[r][c] after recursion — corrupts the grid so later start cells misbehave.
- Forgetting to null the terminal word — the same word appears multiple times in the output.
- Rebuilding the matched string by concatenation during DFS instead of storing it on the terminal node — extra work and bug surface.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewclass Solution { private static class TrieNode { TrieNode[] children = new TrieNode[26]; String word = null; }Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Forgetting to restore the visited cell
Marking board[r][c] = '#' blocks cell reuse within a path, but if you don't restore it on the way back, the cell stays blocked for every other path and start cell — words that legitimately need it are missed.
Explore the code comparisonHide the code comparison
Forgetting to restore the visited cell
Marking board[r][c] = '#' blocks cell reuse within a path, but if you don't restore it on the way back, the cell stays blocked for every other path and start cell — words that legitimately need it are missed.
Don't write this
board[r][c] = '#';
dfs(board, r + 1, c, next, result);
dfs(board, r - 1, c, next, result);
// missing restore — board permanently mutatedWrite this instead
board[r][c] = '#';
dfs(board, r + 1, c, next, result);
dfs(board, r - 1, c, next, result);
// ... other directions ...
board[r][c] = ch; // restore on backtrack2Duplicate words in the output
If the same word can be traced via two paths and you never clear its marker, it gets collected twice. Nulling next.word on first collection both dedupes and prunes future work.
Explore the code comparisonHide the code comparison
Duplicate words in the output
If the same word can be traced via two paths and you never clear its marker, it gets collected twice. Nulling next.word on first collection both dedupes and prunes future work.
Don't write this
if nxt.word is not None:
result.append(nxt.word)
# marker left set — same word can be added againWrite this instead
if nxt.word is not None:
result.append(nxt.word)
nxt.word = None # collected once, then cleared3Stopping the DFS after collecting a word
When a node holds a word that is also a prefix of a longer word ("oat" inside "oath"), returning right after collecting would miss the longer word. Collect, then keep descending.
Explore the code comparisonHide the code comparison
Stopping the DFS after collecting a word
When a node holds a word that is also a prefix of a longer word ("oat" inside "oath"), returning right after collecting would miss the longer word. Collect, then keep descending.
Don't write this
if nxt.word is not None:
result.append(nxt.word)
return # WRONG — "oath" never reached after "oat"Write this instead
if nxt.word is not None:
result.append(nxt.word)
nxt.word = None
# fall through and continue DFS into neighboursRevise
Carry the pattern into the interview
Pattern to remember
Trie of the word list + single grid DFS, pruned by the trie
Code pattern to memorize
build trie of words (store word at terminal node)
for each cell (r,c):
dfs(r, c, trie.root)
dfs(r, c, node):
ch = board[r][c]
if ch not in node.children: return # PRUNE
nxt = node.children[ch]
if nxt.word: collect(nxt.word); nxt.word = None # dedupe
mark visited; explore 4 neighbours; unmark
if nxt has no children: prune leafRules to remember
- 1Build one trie of all words; DFS the grid once — never re-scan per word.
- 2Store the full word string at the terminal node so you collect without rebuilding the path.
- 3After collecting a word, null out its terminal marker to avoid duplicate results.
- 4Mark the current cell visited before recursing, restore it after (classic backtracking).
- 5Prune trie leaves: once a node has no children left, detach it so future DFS stops earlier.
Use this pattern when
- Search a grid/board for MANY target words at once
- Multiple-pattern matching where patterns share prefixes
- A single-word DFS exists but is called once per word (too slow)
- Any 'find all dictionary words inside a structure' task
Choose another pattern when
- Only ONE word to find → plain Word Search I DFS, no trie needed
- Words share no prefixes and are few → the trie overhead may not pay off
- You can match on a 1D string → Aho-Corasick / KMP may fit better
- Grid is enormous but word list tiny → per-word search might actually win
Interview takeaway
The trie flips the loop: instead of 'for each word, search the grid', it's 'walk the grid once, let the trie tell you which words are still possible'. Shared prefixes are explored once, dead branches die immediately, and pruning collected words/empty leaves keeps the DFS lean. It's the showcase of trie-as-a-pruning-oracle.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical Trie-as-pruning-oracle over a grid DFS. Recognise it when you must find MANY words in a structure: merge the words into a trie, traverse once, and let missing children prune dead branches. Same family as Word Search I (single word) and Add-and-Search-Words (trie + DFS).
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Word SearchComing soonThe single-word baseline — one DFS, no trie. Word Search II generalises it to many words.
- Implement Trie (Prefix Tree)Build this trie first; storing the word on the terminal node is the small tweak here.
- Design Add and Search Words Data StructureAlso trie + DFS, but the branching comes from '.' wildcards rather than grid neighbours.
- Word SquaresComing soonTrie of prefixes again — backtrack row by row, using the trie to find words with a required prefix.