Find a word without reusing a cell
“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.
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.
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.
"""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'