Word Search II (Trie + Backtrack)
Build Trie - insert 'eat'
Insert word 'eat' into the trie. Root → e(id=1) → a(id=2) → t(id=3, word='eat'). Three nodes created along the path.
for w in words:
node = root
for c in w:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = wBuild Trie - insert 'oath'
Insert word 'oath'. Root → o(id=4) → a(id=5) → t(id=6) → h(id=7, word='oath'). Four nodes created.
for c in w:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = wDFS from cell (1,1)='t' - no trie match, prune
Start DFS at board[1][1]='t'. Check if root has child 't' - it does not. Prune immediately without exploring neighbors.
def backtrack(r, c, node):
letter = board[r][c]
currNode = node.children.get(letter)
if not currNode:
return # pruneDFS from cell (1,3)='e' - match 'e', continue
Start DFS at board[1][3]='e'. Root has child 'e' (id=1). Mark board[1][3]='#', move to trie node e.
board[r][c] = '#'
for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
nr, nc = r + dr, c + dcDFS from (1,3)→(0,3)='a' - match 'a', continue
From board[1][3]='#' (was 'e'), check neighbor board[0][3]='a'. Trie node e has child 'a' (id=2). Mark board[0][3]='#', move to trie node a.
currNode = node.children.get(letter)
if not currNode: return
board[r][c] = '#'DFS from (0,3)→(1,3) blocked - try (0,2)='a' - no 't'
(1,3) is '#' (already visited). Try neighbor (0,2)='a' - trie node 'a' has no child 'a'. Try (0,2)='a' is also not 't'. Need 't' but only neighbors are '#' or 'a'. No match on any side - backtrack.
board[r][c] = letter # restore after backtrack
if not currNode.children:
node.children.pop(letter) # prune trieDFS from cell (1,3)='e' → neighbor (1,2)='a' → (1,1) was 't' - find 'eat'!
Restart from board[1][3]='e'. Move to neighbor (1,2)='a' (trie: e→a). From (1,2) move to neighbor (1,1)='t' (trie: a→t, word='eat'). Word found! Append 'eat' to result. Set currNode.word=None to prevent re-finding.
if currNode.word:
result.append(currNode.word)
currNode.word = None # prevent duplicateDFS from cell (0,0)='o' - match 'o', start 'oath' search
Scan all board cells for new starting characters. Cell (0,0)='o' matches root child 'o' (id=4). Mark board[0][0]='#', move to trie node o.
for r in range(rows):
for c in range(cols):
backtrack(r, c, root)(0,0)→(1,0)='e' - no 'a' child for 'o' branch
From (0,0)='#', try neighbor (1,0)='e'. Trie node 'o' has no 'e' child. Try (0,1)='a' - trie node 'o' has 'a' child (id=5). Match! Move to (0,1).
currNode = node.children.get(letter)
if not currNode:
return(0,1)→(1,1)='t' - match 't', one step closer
From (0,1)='a', check neighbors. (1,1)='t' - trie node 'a' (child of 'o') has 't' child (id=6). Match! Mark (1,1)='#', move to trie node t.
board[r][c] = '#'
backtrack(nr, nc, currNode)(1,1)→(2,1)='h' - find 'oath'! Prune trie node
From (1,1)='t', check neighbor (2,1)='h'. Trie node 't' has 'h' child (id=7, word='oath'). Word found! Append 'oath' to result. Set word=None. Node 7 has no children → prune it from trie (pop 'h' from t's children).
if currNode.word:
result.append(currNode.word)
currNode.word = None
...
if not currNode.children:
node.children.pop(letter)from typing import List
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
class Solution:
def buildTrie(self, words):
root = TrieNode()
for w in words:
node = root
for c in w:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = w
return root
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = self.buildTrie(words)
rows, cols = len(board), len(board[0])
result = []
def backtrack(r, c, node):
letter = board[r][c]
currNode = node.children.get(letter)
if not currNode:
return
if currNode.word:
result.append(currNode.word)
currNode.word = None
board[r][c] = '#'
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] != '#':
backtrack(nr, nc, currNode)
board[r][c] = letter
if not currNode.children:
node.children.pop(letter)
for r in range(rows):
for c in range(cols):
backtrack(r, c, root)
return result
# Driver code
if __name__ == '__main__':
board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
words = ['oath','pea','eat','rain']
sol = Solution()
print(sol.findWords(board, words))Key Takeaways
Without a trie you'd check each word against each board path separately, which is much slower.
The restore step (board[r][c] = letter) after backtracking lets other DFS paths reuse the cell.
This optimization is subtle but powerful - the trie gets smaller as words are found.
