Files
codetutor/backend/data/questions/word-search.yaml

198 lines
7.7 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
title: Word Search
slug: word-search
difficulty: medium
leetcode_id: 79
leetcode_url: https://leetcode.com/problems/word-search/
categories:
- arrays
- recursion
patterns:
- backtracking
- dfs
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
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
&nbsp;
**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
&nbsp;
**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)
&nbsp;
**Step 4: Return result**
- If any DFS call returns True, the word exists
- If all starting points fail, return False
&nbsp;
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.