HARD Lab · Matrix · 40

79. Word Search

Given a 2D board of characters, determine whether a target word can be spelled by following a path of adjacent cells (up/down/left/right, no reuse). The key is a clean backtracking DFS — borrow each cell by temporarily marking it visited, recurse into all four neighbours, and restore on the way out.

MediumBacktrackingMatrix DFSTypeScript

PROBLEM What we're solving

Given an m×n grid of characters and a string word, return true if the word exists in the grid spelled out along a path of horizontally/vertically adjacent cells, where each cell may be used at most once. Example: board = ["ABCE","SFCS","ADEE"], word = "ABCCED" true (the path goes A→B→C→C→E→D).

KEY IDEA Borrow the cell, recurse, then give it back

Insight → a cell can only be used once per path, so before recursing into a neighbor temporarily overwrite the current cell with a sentinel (e.g. '#') so future calls in the same branch can't visit it again. After all four recursive calls return, restore the original character — this lets completely independent starting paths reuse every cell freely.

RECIPE Try every start, DFS with undo

  • 0 · Scan for start. Iterate every (r,c); call dfs(r, c, 0) whenever board[r][c] === word[0].
  • 1 · Base case. If depth === word.length, we consumed all characters — return true.
  • 2 · Bounds & match check. Out of bounds, already-visited sentinel, or character mismatch → return false immediately.
  • 3 · Borrow. Save tmp = board[r][c], write board[r][c] = '#' to block revisits within this path.
  • 4 · Recurse. Call dfs for all four neighbours with depth + 1. If any returns true, restore and propagate true.
  • 5 · Restore. Write board[r][c] = tmp — the cell is available again for other branches.
Classic confusion → forgetting to restore the cell before propagating the early true. If you return without restoring, subsequent calls from different starting cells see a permanently-corrupted board. Always restore before every return path from dfs.

COST Complexity & pruning

Brute force (all permutations)
O((R·C)!)
Combinatorial explosion — entirely impractical.
Backtracking DFS
O(R·C · 3^L)
L = word length; 3 choices per step (not 4; we never revisit the parent).

Space is O(L) for the recursion stack (depth bounded by word length). In the worst case every cell matches and we still try every path, but in practice early mismatch prunes the tree aggressively.

Pattern transfer → the same borrow/restore pattern appears in Word Search II (use a Trie to prune multiple words simultaneously), N-Queens (mark attacked rows/cols/diagonals and unmark on backtrack), Combination Sum(pick an element, recurse, then skip it), and any problem that asks "does any valid arrangement exist?" on a shared mutable structure.

RUN IT DFS from each cell, backtrack on mismatch

step 0 / 9
STARTSearch the 3×4 board for "ABCCED". Try each cell as a starting point; backtrack on mismatch.
A
B
C
E
S
F
C
S
A
D
E
E
Word
ABCCED
pos: (-1, -1)
depth: 0
current cell (DFS frontier)matched path so farfull word foundmismatch / backtrack
slowfast

TYPESCRIPT The solution, annotated

wordSearch.ts
function exist(board: string[][], word: string): boolean {
  const R = board.length;
  const C = board[0].length;
  const DIRS: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];

  function dfs(r: number, c: number, depth: number): boolean {
    if (depth === word.length) return true;
    if (r < 0 || r >= R || c < 0 || c >= C) return false;
    if (board[r][c] !== word[depth]) return false;

    const tmp = board[r][c];
    board[r][c] = '#';           // mark visited

    for (const [dr, dc] of DIRS) {
      if (dfs(r + dr, c + dc, depth + 1)) {
        board[r][c] = tmp;       // restore before returning
        return true;
      }
    }

    board[r][c] = tmp;           // backtrack
    return false;
  }

  for (let r = 0; r < R; r++) {
    for (let c = 0; c < C; c++) {
      if (dfs(r, c, 0)) return true;
    }
  }
  return false;
}

Reading it block by block

Lines 7–9 — three cheap exits.Before doing any work, bail if depth equals the word length (success), out of bounds, or the cell character doesn't match the current character we need. These prune the tree before any mutation.
Lines 11–12 — borrow the cell. Save the original character in tmp, then overwrite with '#'. Because the character check is done above, any future call into this cell during the same DFS path will see '#' and fail the match check — no extra visited set needed.
Lines 14–18 — recurse into neighbours. Loop over the four direction offsets. If any recursive call returns true, restore tmp before propagating — the board must be clean for the caller.
Line 20 — unconditional restore. Whether all four neighbours failed or we short- circuited on success, write tmp back so the cell is reusable by entirely different starting paths in the outer loops.
Lines 23–27 — outer scan. Launch dfs from every cell. The first true bubbles up immediately thanks to the early-return on success; otherwise return false after exhausting all starts.
Complexity → O(R × C  ×  3L) time: R × C starts, each spawning a DFS tree of depth L where at every level we branch into at most 3 new cells (the parent is already marked). Space is O(L) for the recursion stack.

INTERVIEWFollow-ups they'll ask

  • "Word Search II — find ALL words from a dictionary?" Build a Trie from the word list and run a single DFS pass; prune entire subtrees when no word in the Trie shares the prefix.
  • "Can you do it without modifying the board?" Replace the sentinel trick with an explicit visited: boolean[][]array (same O(R × C) space). The borrow/restore pattern is usually preferred for its elegance and lower constant.
  • "What if diagonal moves are allowed?" Expand DIRS from 4 to 8 entries; complexity becomes O(R × C  ×  7L).
  • "Early termination: when is the board obviously too small?" If word.length > R * C, return false immediately — there aren't enough cells to spell it without reuse.
  • "What are the edge cases?" Single-cell board, word of length 1 (just check if that character exists), word longer than the total cells, board with all identical characters.

MNEMONIC The one-liner

"Borrow the cell with #, recurse into neighbours, return the cell on the way out."

TRIGGERS When you see ___ → reach for ___

"word exists in grid / adjacent path"backtracking DFS + cell borrow
"each cell used at most once"overwrite with sentinel, restore after recurse
"find any valid arrangement on shared state"backtrack: mutate → recurse → undo
"all words from dictionary in grid"Trie + backtracking DFS (Word Search II)

SKELETON The reusable shape

skeleton.ts
function exist(board: string[][], word: string): boolean {
  const R = board.length, C = board[0].length;
  const DIRS: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];

  function dfs(r: number, c: number, depth: number): boolean {
    if (depth === word.length) return true;
    if (r < 0 || r >= R || c < 0 || c >= C) return false;
    if (board[r][c] !== word[depth]) return false;
    const tmp = board[r][c];
    board[r][c] = '#';
    for (const [dr, dc] of DIRS) {
      if (dfs(r + dr, c + dc, depth + 1)) { board[r][c] = tmp; return true; }
    }
    board[r][c] = tmp;
    return false;
  }

  for (let r = 0; r < R; r++)
    for (let c = 0; c < C; c++)
      if (dfs(r, c, 0)) return true;
  return false;
}

FLASHCARDS Tap to flip

Why overwrite the cell with # instead of a visited set?
The cell itself is the state — overwriting is O(1) and avoids an extra data structure. Restoring it on backtrack means different DFS paths each see a clean board.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
For board ["ABCE","SFCS","ADEE"] and word = "ABCCED", what does the algorithm return?
QUESTION 02
Why do we overwrite the visited cell with a sentinel character before recursing?
QUESTION 03
What is the correct time complexity?
QUESTION 04
What MUST happen just before propagating an early true return from inside the DFS?
QUESTION 05
A word of length L = 20 is searched in a 3×3 board (9 cells). What should the algorithm do first?
QUESTION 06
How does Word Search II (find all words) improve on running Word Search once per word?
QUESTION 07
If we call dfs(r, c, depth) and board[r][c] !== word[depth], the correct action is: