Find a word without reusing a cellLESSON 2.31 · 31 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 57 of 252
LESSON 2.31 · 31 OF 43 IN CHAPTERTry it, then open the solution

Find a word without reusing a cell

THE PROBLEM

“A puzzle board contains one character per cell. A word is present if a path of horizontal or vertical neighboring cells spells it. A cell cannot appear twice in the same path. Return whether any path exists, and leave the board usable for another query. Which visited state belongs to one attempt rather than all attempts?”

Write this:

def word_search(board, word):
    ...

Constructed practice question. Prerequisite: backtracking and grid modeling. Unlike ordinary reachability, the allowed next moves depend on which cells the current path already consumed.

Contract Required behavior
Input Rectangular board of one-character strings; case-sensitive string word
Output Boolean existence of an orthogonal path with no repeated cell
Boundaries Empty word is true even on empty board; nonempty word on empty board is false
Failure Ragged board or invalid cells/word type raise ValueError
Scope No diagonal moves, wildcard characters, mutation, or all-path enumeration
Optional refresher · the underlying tool

Backtracking marks only cells in the current candidate path. Restore a mark before exploring another start; otherwise one failed path can poison a valid one:

used = set()
used.add((0, 0))      # choose a cell
# explore neighbors here
used.remove((0, 0))   # undo when returning

On the one-row board [["A","B"]], "AB" is present but "ABA" is impossible without reusing the only A. State whether diagonal movement is allowed.

A design choice worth saying aloud

used_in_path belongs to the current search branch, not to the entire board. A failed route must release its cells before another route starts. If you mark by mutating the board, restore cells even on early returns; a separate set avoids altering caller-owned input.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: Boolean existence of an orthogonal path with no repeated cell.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
board contains an orthogonal spelling
Expected result
True

What it is testing: A path may turn.

02 · No cell reuse

Input / starting state
word needs the same cell twice
Expected result
False

What it is testing: Visited state belongs to the current path.

03 · Backtrack

Input / starting state
first matching prefix dead-ends; later start succeeds
Expected result
True

What it is testing: Restore state before exploring alternatives.

04 · Empty word

Input / starting state
any board, including empty
Expected result
True

What it is testing: No cells are required.

05 · Empty board

Input / starting state
nonempty word
Expected result
False

What it is testing: Valid but unsatisfiable.

06 · Invalid/atomic

Input / starting state
ragged board or multi-character cell
Expected result
ValueError; board unchanged

What it is testing: Validation and restoration are observable.

For each case, show which branch or state change produces that result.

For rows ABCE / SFCS / ADEE, ABCCED and SEE are present; ABCB is absent because its apparent final B would reuse the earlier B. In a two-cell board AB, ABA is false. Ask whether returning one coordinate witness would be more useful; the supplied API returns only existence.

Diagram: Test-case scenarios to settle before coding

Implement independently. Explain why a visited set shared permanently across all starting cells is incorrect, and name the state restored after a failed branch.

Solution, path-local state, and follow-ups

A baseline enumerates all simple board paths and compares their strings afterward. Most prefixes cannot match the word, so this generates unnecessary work. Ordinary BFS with a global visited set also does not solve the problem: reaching a cell at one word position or with one used-cell set does not dominate another attempt.

Start a search at each cell matching the first character. Extend only to an in-bounds, unused neighbor matching the next character. Add that neighbor to the path-local visited set; remove it when the branch returns. Stop as soon as path length equals word length. The explicit stack stores each frame's next direction, making the restore step visible and avoiding recursion limits.

stack cells are distinct, spell exactly the consumed word prefix, and equal the visited set. Popping a failed frame must remove precisely its cell. The board itself never changes. Because each extension consumes another character, search terminates after at most L matched cells for word length L.

Search event Current prefix Used-cell rule
Start at A A Only that A is reserved
Extend to B then C ABC A, B, C unavailable to this branch
Try B again ABCB Reject: existing B is already used
Return from C AB C becomes available to sibling branches

For V board cells and L>0, a loose bound is O(V × 4^L) time; after the first step at most three directions can extend a path because the immediate predecessor is used, giving O(V × 3^L) up to constant factors. Validation adds O(V). Auxiliary space is O(min(V,L)) for stack/set; no full board copy is made. L>V immediately fails. This remains exponential rather than becoming linear merely by using DFS.

Follow-up 1 — cells may be reused. Predict ABA on AB: it becomes true. Remove the used-cell constraint; state (row,column,word_index) now suffices for memoization because earlier path history no longer limits legal continuations.

Diagram: Test-case scenarios to settle before coding

Follow-up 2 — find many dictionary words. Carry a trie node rather than one word index. Missing child edges prune all words sharing that impossible prefix; terminal markers emit matches. Still retain path-local cell usage, and deduplicate words reached through multiple board paths. The trie changes prefix work, not the worst-case combinatorial nature of board paths.

Senior depth distinguishes graph visitation from backtracking state and verifies restoration. Lead depth defines query budgets for adversarial repeated-letter boards.

Reference: solution.py (download file, source below); tests cover reuse rejection, alternative branches, unchanged input, diagonal rejection, empties, and a deep iterative path.

solution.py · solution.py
"""Find an orthogonal word path without reusing cells or changing the board."""


def word_search(board, word):
    if any(len(row) != (len(board[0]) if board else 0) for row in board):
        raise ValueError("rectangular board required")
    if any(not isinstance(c, str) or len(c) != 1 for row in board for c in row):
        raise ValueError("single-character cells required")
    if not isinstance(word, str):
        raise ValueError("word must be a string")
    if not word:
        return True
    if not board or not board[0] or len(word) > len(board) * len(board[0]):
        return False
    rows, cols = len(board), len(board[0])
    directions = ((1, 0), (0, 1), (-1, 0), (0, -1))
    for row in range(rows):
        for col in range(cols):
            if board[row][col] != word[0]:
                continue
            visited = {(row, col)}
            # Frame: row, col, next direction. Stack depth is matched length.
            stack = [[row, col, 0]]
            while stack:
                if len(stack) == len(word):
                    return True
                r, c, direction = stack[-1]
                if direction == 4:
                    stack.pop()
                    visited.remove((r, c))
                    continue
                stack[-1][2] += 1
                dr, dc = directions[direction]
                neighbor = nr, nc = r + dr, c + dc
                if (0 <= nr < rows and 0 <= nc < cols and neighbor not in visited
                        and board[nr][nc] == word[len(stack)]):
                    visited.add(neighbor)
                    stack.append([nr, nc, 0])
    return False
python -m unittest discover -s curriculum/01-code/02-data-structures-algorithms/problems/32-word-search -p 'test_*.py'