206 lines
8.0 KiB
YAML
206 lines
8.0 KiB
YAML
title: Word Search
|
||
slug: word-search
|
||
difficulty: medium
|
||
leetcode_id: 79
|
||
leetcode_url: https://leetcode.com/problems/word-search/
|
||
categories:
|
||
- arrays
|
||
- recursion
|
||
patterns:
|
||
- slug: backtracking
|
||
is_optimal: false
|
||
- slug: dfs
|
||
is_optimal: true
|
||
|
||
function_signature: "def exist(board: list[list[str]], word: str) -> bool:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { board: [["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], word: "ABCCED" }
|
||
expected: true
|
||
- input: { board: [["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], word: "SEE" }
|
||
expected: true
|
||
- input: { board: [["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], word: "ABCB" }
|
||
expected: false
|
||
hidden:
|
||
- input: { board: [["A"]], word: "A" }
|
||
expected: true
|
||
- input: { board: [["A", "B"], ["C", "D"]], word: "ABCD" }
|
||
expected: false
|
||
- input: { board: [["A", "B"], ["C", "D"]], word: "ABDC" }
|
||
expected: true
|
||
- input: { board: [["A"]], word: "B" }
|
||
expected: false
|
||
- input: { board: [["A", "A", "A"], ["A", "A", "A"], ["A", "A", "A"]], word: "AAAAAAAAA" }
|
||
expected: true
|
||
- input: { board: [["C", "A", "A"], ["A", "A", "A"], ["B", "C", "D"]], word: "AAB" }
|
||
expected: true
|
||
|
||
description: |
|
||
Given an `m × 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**.
|
||
|
||
constraints: |
|
||
- `m == board.length`
|
||
- `n == board[i].length`
|
||
- `1 <= m, n <= 6`
|
||
- `1 <= word.length <= 15`
|
||
- `board` and `word` consist of only lowercase and uppercase English letters
|
||
|
||
examples:
|
||
- input: 'board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"'
|
||
output: "true"
|
||
explanation: "Path: A(0,0) → B(0,1) → C(0,2) → C(1,2) → E(2,2) → D(2,1)"
|
||
- input: 'board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"'
|
||
output: "true"
|
||
explanation: "Path: S(1,3) → E(2,3) → E(2,2)"
|
||
- input: 'board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"'
|
||
output: "false"
|
||
explanation: "Would require reusing the 'B' cell at (0,1)."
|
||
|
||
explanation:
|
||
intuition: |
|
||
Imagine walking through a maze of letters, trying to spell out a word. At each step, you can move up, down, left, or right to an adjacent cell. But there's a rule: you can't step on the same cell twice.
|
||
|
||
This is a classic **backtracking** problem. We try a path, and if it leads to a dead end (wrong character or no valid moves), we **backtrack** — undo our steps and try a different direction.
|
||
|
||
Think of it like this:
|
||
1. Start from any cell that matches the first character
|
||
2. From there, try to find the second character in any adjacent cell
|
||
3. Mark cells as "visited" to prevent reuse
|
||
4. If we hit a dead end, **unmark** the cell and try another path
|
||
5. If we match all characters, we've found the word!
|
||
|
||
The key insight is that backtracking requires **restoring state** after each failed attempt.
|
||
|
||
approach: |
|
||
We solve this using **DFS with Backtracking**:
|
||
|
||
**Step 1: Try every cell as a starting point**
|
||
|
||
- Iterate through all cells in the grid
|
||
- For each cell, attempt to find the word starting there
|
||
|
||
|
||
|
||
**Step 2: Define the DFS function**
|
||
|
||
- `dfs(row, col, index)` returns True if we can find `word[index:]` starting from `(row, col)`
|
||
- Base case: if `index == len(word)`, we've matched everything — return True
|
||
- Boundary check: if out of bounds, return False
|
||
- Character check: if `board[row][col] != word[index]`, return False
|
||
|
||
|
||
|
||
**Step 3: Mark, explore, and unmark**
|
||
|
||
- **Mark**: Temporarily change `board[row][col]` to `'#'` to prevent reuse
|
||
- **Explore**: Recursively check all four directions with `index + 1`
|
||
- **Unmark**: Restore `board[row][col]` to its original value (backtrack)
|
||
|
||
|
||
|
||
**Step 4: Return result**
|
||
|
||
- If any DFS call returns True, the word exists
|
||
- If all starting points fail, return False
|
||
|
||
|
||
|
||
The unmarking step is crucial — it allows other paths to use the same cell.
|
||
|
||
common_pitfalls:
|
||
- title: Not Restoring Visited State
|
||
description: |
|
||
After exploring a path, you **must** restore the cell's original value. Otherwise, other paths can't use that cell.
|
||
|
||
```python
|
||
# WRONG: Cell stays marked forever
|
||
board[r][c] = '#'
|
||
result = dfs(...)
|
||
return result
|
||
|
||
# RIGHT: Restore after exploring
|
||
board[r][c] = '#'
|
||
result = dfs(...)
|
||
board[r][c] = original_value # Backtrack!
|
||
return result
|
||
```
|
||
wrong_approach: "Only marking cells, never restoring"
|
||
correct_approach: "Store original value, mark, explore, restore"
|
||
|
||
- title: Checking Word Completion Too Late
|
||
description: |
|
||
Check if `index == len(word)` **before** bounds and character checks. Otherwise, when we've matched all characters, we might return False due to being "out of bounds" at the next position.
|
||
wrong_approach: "Checking bounds/character before word completion"
|
||
correct_approach: "if index == len(word): return True # Check first!"
|
||
|
||
- title: Not Trying All Directions
|
||
description: |
|
||
You must explore all four directions: up, down, left, right. Missing any direction means missing potential valid paths.
|
||
|
||
Use short-circuit OR: `dfs(r+1,c) or dfs(r-1,c) or dfs(r,c+1) or dfs(r,c-1)`
|
||
wrong_approach: "Only checking some directions"
|
||
correct_approach: "Explore all four orthogonal directions"
|
||
|
||
key_takeaways:
|
||
- "**Backtracking = DFS + state restoration**: Mark before recursion, unmark after"
|
||
- "**Early termination**: Return True as soon as the word is found"
|
||
- "**In-place marking**: Using `'#'` to mark cells avoids extra space for a visited set"
|
||
- "**Small constraints enable brute force**: With m, n ≤ 6 and word ≤ 15, exponential exploration is acceptable"
|
||
|
||
time_complexity: "O(m × n × 3^L). We try each cell as a start, and from each cell, we explore up to 3 directions (excluding where we came from) for L characters."
|
||
space_complexity: "O(L). The recursion stack depth equals the word length L."
|
||
|
||
solutions:
|
||
- approach_name: DFS with Backtracking
|
||
is_optimal: true
|
||
code: |
|
||
def exist(board: list[list[str]], word: str) -> bool:
|
||
rows, cols = len(board), len(board[0])
|
||
|
||
def dfs(r: int, c: int, i: int) -> bool:
|
||
# Base case: found all characters
|
||
if i == len(word):
|
||
return True
|
||
|
||
# Boundary check
|
||
if r < 0 or r >= rows or c < 0 or c >= cols:
|
||
return False
|
||
|
||
# Character mismatch
|
||
if board[r][c] != word[i]:
|
||
return False
|
||
|
||
# Mark cell as visited (temporarily)
|
||
original = board[r][c]
|
||
board[r][c] = '#'
|
||
|
||
# Explore all four directions
|
||
found = (
|
||
dfs(r + 1, c, i + 1) or # down
|
||
dfs(r - 1, c, i + 1) or # up
|
||
dfs(r, c + 1, i + 1) or # right
|
||
dfs(r, c - 1, i + 1) # left
|
||
)
|
||
|
||
# Restore cell (backtrack)
|
||
board[r][c] = original
|
||
|
||
return found
|
||
|
||
# Try every cell as starting point
|
||
for r in range(rows):
|
||
for c in range(cols):
|
||
if dfs(r, c, 0):
|
||
return True
|
||
|
||
return False
|
||
explanation: |
|
||
**Time Complexity:** O(m × n × 3^L) — Each starting cell can explore 3 directions per character.
|
||
|
||
**Space Complexity:** O(L) — Recursion depth equals word length.
|
||
|
||
We try each cell as a starting point. DFS matches characters one by one, marking cells to prevent reuse. After exploring, we restore the cell's value (backtrack) to allow other paths to use it. Short-circuit OR provides early termination.
|