212. Word Search II
- Array
- String
- Backtracking
- Trie
- Matrix
Intuition
Running the single-word search 30,000 times is the obvious approach and far too slow: each run re-walks the same board prefixes. The fix is to turn the word list inside out. Put the words in a trie, then do one DFS per starting cell that descends the board and the trie together. Words sharing a prefix share that descent, and a path dies the moment the board letter has no matching trie child.
That alone is not enough on the worst inputs. A 12x12 board of nothing but a with thousands of a-heavy words still re-explores the same region for every surviving word. Two prunes fix it:
- Letter multiplicity. A word needing four
zon a board with one can never match. Counting the board's letters once and filtering the word list is linear in the total word length and removes most candidates on adversarial inputs. - Shrink the trie as it is consumed. When a word is found, remove it from its node. After a node's recursive calls return, if it has no children and no word, delete it from its parent. Found and dead branches physically disappear, so later starting cells do not revisit them. This is the difference between passing and timing out.
Visited cells are handled by overwriting the board cell with a sentinel and restoring it on the way out, which avoids a parallel visited array.
Approach
- Count the board's letters; drop any word whose letter requirements exceed it.
- Insert the remaining words into a trie. Store the full word at its terminal node.
- For each cell whose letter is a child of the root, DFS:
- descend to the child node for this cell's letter
- if the node holds a word, take it out and record it
- mark the cell visited, recurse into the four neighbours whose letter is a child of the current node, then restore the cell
- if the node is now empty (no children, no word), delete it from its parent
- Return the collected words.
Checking board[x][y] in node before recursing keeps the recursion from being entered on a doomed path, which matters more than it looks: it halves the call count.
Complexity
- Time: O(cells * 4 * 3^(L-1)) in the worst case, where
L <= 10is the longest word, plus O(total word length) to build the trie. The prunes make the practical cost far lower. - Space: O(total word length) for the trie, O(L) recursion depth
Edge Cases
- A word longer than the number of cells can never fit; the multiplicity filter catches most such cases, and the trie walk catches the rest
"abcb"on a 2x2 board reuses a cell, so it must not match (Example 2); the sentinel marking is what forbids that- Duplicate results: a word reachable by several paths must appear once, which popping it from the node guarantees
- A word that is a prefix of another (
"ab"and"abc") → both reported, since the shorter one's node still has children and is not pruned - Single-cell board, or a single-character word
- Worst case for pruning: every cell the same letter with many same-letter words
- Restoring the board cell after recursion is required; the caller may inspect the board afterwards
Code
from collections import Counter
from typing import Dict, List
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
"""
212. Word Search II
Time: O(cells * 4 * 3^(L-1)) worst case, plus O(total word length) to build
Space: O(total word length)
"""
WORD = "$"
rows, cols = len(board), len(board[0])
# a word needing more of a letter than the board holds can never match
available = Counter(ch for row in board for ch in row)
candidates = [
w for w in words
if not (Counter(w) - available)
]
trie: Dict = {}
for word in candidates:
node = trie
for ch in word:
node = node.setdefault(ch, {})
node[WORD] = word
found = []
def dfs(r: int, c: int, parent: Dict) -> None:
ch = board[r][c]
node = parent[ch]
word = node.pop(WORD, None)
if word is not None:
found.append(word)
board[r][c] = "#" # mark visited in place
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] in node:
dfs(nr, nc, node)
board[r][c] = ch
# drop exhausted branches so later starts never revisit them
if not node:
del parent[ch]
for r in range(rows):
for c in range(cols):
if board[r][c] in trie:
dfs(r, c, trie)
return foundimport java.util.ArrayList;
import java.util.List;
/**
* 212. Word Search II
* Time: O(cells * 4 * 3^(L-1)) worst case, plus O(total word length) to build
* Space: O(total word length)
*/
class Solution {
private static class TrieNode {
TrieNode[] children = new TrieNode[26];
String word;
int childCount; // kept so pruning is O(1) instead of scanning 26
}
private char[][] board;
private int rows, cols;
private List<String> found;
public List<String> findWords(char[][] board, String[] words) {
this.board = board;
this.rows = board.length;
this.cols = board[0].length;
this.found = new ArrayList<>();
// a word needing more of a letter than the board holds can never match
int[] available = new int[26];
for (char[] row : board) {
for (char ch : row) {
available[ch - 'a']++;
}
}
TrieNode root = new TrieNode();
int[] need = new int[26];
for (String word : words) {
java.util.Arrays.fill(need, 0);
boolean possible = true;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (++need[idx] > available[idx]) {
possible = false;
break;
}
}
if (possible) {
insert(root, word);
}
}
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (root.children[board[r][c] - 'a'] != null) {
dfs(r, c, root);
}
}
}
return found;
}
private void insert(TrieNode root, String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.children[idx] == null) {
node.children[idx] = new TrieNode();
node.childCount++;
}
node = node.children[idx];
}
node.word = word;
}
private void dfs(int r, int c, TrieNode parent) {
char ch = board[r][c];
int idx = ch - 'a';
TrieNode node = parent.children[idx];
if (node.word != null) {
found.add(node.word);
node.word = null;
}
board[r][c] = '#'; // mark visited in place
final int[][] steps = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] step : steps) {
int nr = r + step[0], nc = c + step[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) {
continue;
}
char next = board[nr][nc];
if (next != '#' && node.children[next - 'a'] != null) {
dfs(nr, nc, node);
}
}
board[r][c] = ch;
// drop exhausted branches so later starts never revisit them
if (node.childCount == 0 && node.word == null) {
parent.children[idx] = null;
parent.childCount--;
}
}
}