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
    - 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

    &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.
