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.