#79 - Word Search
Word Search
- Difficulty: Medium
- Topics: Array, String, Backtracking, Depth-First Search, Matrix
- Link: https://leetcode.com/problems/word-search/
Problem Description
Given an m x n
grid of characters board
and a string word
, return true
if word
exists in the grid.
The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once.
Example 1:
Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
Output: true
Example 2:
Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
Output: true
Example 3:
Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
Output: false
Constraints:
m == board.length
n = board[i].length
1 <= m, n <= 6
1 <= word.length <= 15
board
andword
consists of only lowercase and uppercase English letters.
Follow up: Could you use search pruning to make your solution faster with a larger board
?
Solution
1. Problem Deconstruction
Technical Restatement:
Given an matrix board
of characters and a string word
, determine if word
exists contiguously in the matrix. Contiguous means adjacent via horizontal/vertical moves (4-directional connectivity) without reusing any cell. Formally, there must exist a sequence of indices where:
- and are adjacent under
- All are distinct
Beginner-Friendly Restatement:
Imagine a grid of letters. You start at any cell and move up, down, left, or right to adjacent cells. Can you spell the target word by collecting letters in sequence without stepping on the same cell twice?
Mathematical Formulation:
Let be the matrix, the word of length . Define:
- as all paths where:
- Goal: such that:
Constraint Analysis:
- :
- Limits solution complexity; DFS is feasible since and (worst-case operations, acceptable in Pyton due to pruning).
- Edge: Small grids (e.g., ) require handling single-cell words.
- :
- DFS depth avoids stack overflow.
- Edge: Word longer than (impossible, caught by frequency check).
- Case-sensitive letters:
- ‘a’ ‘A’; must match exactly.
- Edge: Word with letters absent in board (pruned early).
2. Intuition Scaffolding
Analogies:
- Real-World Metaphor (Scavenger Hunt):
- You have a map (grid) of letters. Find a path collecting the word’s letters in order without revisiting locations.
- Gaming Analogy (Path Building in RPGs):
- Like connecting runes on a magic grid to cast a spell, where each rune must be adjacent and uniquely used.
- Math Analogy (Graph Walk):
- Model grid as graph where cells, adjacent cells. Solve: Is a label sequence of a simple path in ?
Common Pitfalls:
- Missing Frequency Pruning:
- If has more 'A’s than the grid, DFS wastes time.
- Forgetting to Unmark Cells:
- Without backtracking cleanup, subsequent DFS calls see modified board.
- Terminating Early on Last Character:
- DFS must return after matching the entire word, not just the start.
- Ignoring Case Sensitivity:
- ‘a’ vs ‘A’ treated as mismatch.
- Optimizing Prematurely for Larger Boards:
- Constraints allow DFS, but for larger grids (follow-up), need advanced pruning (e.g., Trie, meet-in-middle).
3. Approach Encyclopedia
Approach 1: Brute-Force DFS with Backtracking
- Idea: For each cell matching
word[0]
, start DFS to matchword[1:]
via adjacent moves, backtracking if path fails. - Pseudocode:
for i in range(m): for j in range(n): if DFS(i, j, 0) returns True: return True return False def DFS(i, j, k): if (i, j) out of bounds or board[i][j] != word[k]: return False if k == len(word) - 1: return True mark board[i][j] as visited for each adjacent cell (ni, nj): if DFS(ni, nj, k+1): return True unmark board[i][j] return False
- Complexity Proof:
- Time: . Each DFS call branches (excludes parent). steps per path, starting points.
- Space: for recursion stack (depth ).
- Time: . Each DFS call branches (excludes parent). steps per path, starting points.
- Visualization:
Example: word = "AB", board = [["A","B"]] Start at (0,0): k=0: 'A' matches word[0] -> mark (0,0) as '#' Adjacent: (0,1) -> 'B' matches word[1] -> return True Path: (0,0) → (0,1) Board state during DFS: Initial: [A, B] k=0: [#, B] k=1: [#, #] (restored after True)
Approach 2: Optimized DFS with Frequency Pruning
- Idea: Add pre-check: If any char in
word
exceeds its frequency inboard
, returnFalse
early. - Pseudocode:
freq_board = count chars in board freq_word = count chars in word for char in freq_word: if freq_word[char] > freq_board[char]: return False # Then run DFS as in Approach 1
- Complexity Proof:
- Time: , where is alphabet (52 letters). Pruning reduces in practice.
- Space: for frequency counters.
- Visualization:
Example: word = "ZZZ", board = [["A","B"]] freq_board: {'A':1, 'B':1}, freq_word: {'Z':3} 'Z' count in word (3) > board (0) → return False without DFS.
4. Code Deep Dive
Optimal Solution (DFS + Frequency Pruning):
from collections import Counter
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m, n = len(board), len(board[0])
# Frequency pruning
board_chars = [ch for row in board for ch in row]
if Counter(word) - Counter(board_chars): # If word has excess chars
return False
def dfs(i, j, k):
if not (0 <= i < m and 0 <= j < n) or board[i][j] != word[k]:
return False
if k == len(word) - 1:
return True
temp, board[i][j] = board[i][j], '#' # Mark visited
for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]:
ni, nj = i + dx, j + dy
if dfs(ni, nj, k + 1):
board[i][j] = temp # Restore before returning
return True
board[i][j] = temp # Restore on failure
return False
for i in range(m):
for j in range(n):
if dfs(i, j, 0):
return True
return False
Line-by-Line Annotations:
- Lines 5-7: Frequency check.
Counter(word) - Counter(board_chars)
is non-empty ifword
has chars not inboard
or in excess. - Line 9:
dfs
function: Checks(i,j)
forword[k]
match. - Lines 10-11: Base 1: Out-of-bounds or mismatch → fail. Base 2: Entire
word
matched → success. - Line 12: Temporarily mark
board[i][j]
as'#'
to prevent reuse. - Lines 13-16: Recursively check neighbors. If any path succeeds, restore cell and propagate
True
. - Line 17: Restore cell if all neighbors fail.
- Lines 19-22: Try DFS from every starting cell.
Edge Case Handling:
- Example 1 (
"ABCCED"
):- Starts at
(0,0)
, path:(0,0)→(0,1)→(0,2)→(1,2)→(2,2)→(2,1)
.dfs(2,1,5)
returnsTrue
.
- Starts at
- Example 2 (
"SEE"
):- Starts at
(1,3)
, path:(1,3)→(2,3)→(2,2)
.dfs(2,2,2)
returnsTrue
.
- Starts at
- Example 3 (
"ABCB"
):- All paths fail frequency check? No, frequencies suffice, but DFS fails → returns
False
.
- All paths fail frequency check? No, frequencies suffice, but DFS fails → returns
- Single-Letter Word:
k==0
andlen(word)==1
→dfs
returnsTrue
at line 11.
5. Complexity War Room
Hardware-Aware Analysis:
- Worst-case: , → operations.
- In Python, ops/sec → sec (worst-case, but pruning reduces this).
- Memory: stack depth ( frames) → negligible.
Industry Comparison Table:
Approach | Time Complexity | Space Complexity | Readability | Interview Viability |
---|---|---|---|---|
Brute-Force DFS | 9/10 | ❌ Fails | ||
DFS + Pruning | 8/10 | ✅ Optimal | ||
DP + Bitmask | 3/10 | ❌ Exceeds memory |
6. Pro Mode Extras
Variants:
- LC 79 (This Problem): Solved above.
- LC 212 (Word Search II):
- Given a list of words, return all words in the board.
- Solution: Trie + DFS. Insert words into Trie, traverse board to match Trie paths.
- LC 123 (Multi-Transaction Stocks):
- Unrelated but mentioned in template; uses state machine DP.
Interview Cheat Sheet:
- Mention First: “DFS with backtracking and frequency pruning; time, space.”
- Corner Cases:
- Single-cell board with one-letter word.
word
longer than .- Case sensitivity.
- Optimization Hints:
- Reverse
word
if suffix is rarer than prefix (empirically faster). - Use iterative DFS to avoid recursion limits.
- Reverse