Algorithm

Word Search

Backtracking Pattern

Word Search

Given an m x n grid of characters and a string word, return true if the word can be traced on the grid.

A trace is a path of sequentially adjacent cells — horizontally or vertically neighbouring, never diagonally — whose letters spell the word in order. No cell may be used more than once within the same trace.

Only a boolean is required; the path itself is not returned. The search may begin at any cell.

CONSTRAINTS
  • m == board.length, n == board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board and word consist of English letters
  • Matching is case-sensitive: 'A' and 'a' are different characters
EXAMPLE 1
Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
Output: true
The path runs A(0,0), B(0,1), C(0,2), then down to C(1,2), down to E(2,2), and left to D(2,1). A trace may change direction freely — it is not required to keep going the same way.
EXAMPLE 2
Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
Output: true
Starting at S(1,3), down to E(2,3), then left to E(2,2). Note there is another S at (1,0) which leads nowhere — the search must be willing to try every starting cell, not just the first letter match it finds.
EXAMPLE 3
Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
Output: false
A, B and C trace fine, but the final B would require returning to (0,1), which is already part of this path. The no-reuse rule is what makes this false — without it the answer would be true.
EXAMPLE 4
Input: board = [["a"]], word = "a"
Output: true
A single cell matching a single character succeeds with no movement at all. The base case must fire on a fully matched word before any neighbour is examined, or this input fails.
Can a cell be reused within the same word?
No. That restriction is the entire reason this needs backtracking rather than a plain search — cells must be marked while a path uses them and released when it backs out.
Are diagonal moves allowed?
No, only the four orthogonal directions. A variant permitting all eight is common, so it is worth confirming.
Is the matching case-sensitive?
Yes, 'A' and 'a' are distinct. The constraints say English letters rather than lowercase letters specifically, so do not assume a single case.
Do I need to return the path, or may I modify the board?
Only a boolean is needed. Modifying the board in place is the standard trick for marking visited cells, but every mark must be restored — leaving the board altered when the function returns is a side effect the caller will not expect.

Every earlier problem in this section built an abstract path — a list of chosen numbers. Here the path is a route across a grid, and the state being modified is the board itself. That makes the undo step visible in a way the earlier problems did not, which is why this is a good place to see why backtracking is called backtracking.

The search, and why it does not terminate

The obvious approach: try every cell as a start, and from each, follow neighbours matching the next character.

python
def explore(r, c, i):
    if board[r][c] != word[i]:
        return False
    if i == len(word) - 1:
        return True
    for nr, nc in neighbours(r, c):
        if explore(nr, nc, i + 1):        # no record of where we have been
            return True
    return False

This does not merely give wrong answers — it does not finish. Search for "ABAB" on a board containing an adjacent A and B. The search steps A to B, then from B looks at its neighbours and finds the same A, steps back, and repeats forever until the stack overflows.

The missing ingredient is memory of the current path. A cell already used by this trace must not be entered again.

Why a plain visited set is the wrong fix

The reflex from graph traversal is to keep a visited set and never clear it. That is right for reachability problems and wrong here.

The rule is that a cell cannot be reused within one path. It says nothing about different paths. If a route through cell (1,1) fails, some other route may legitimately use (1,1) — and marking it permanently visited would block that, causing valid words to be reported as absent.

So the mark must be scoped to the path currently being explored: set when the path enters a cell, cleared when the path leaves it. That is precisely the choose-explore-un-choose rhythm, with the board as the shared state.

Key Insighta permanent visited set answers "can this cell ever be reached?" A path-scoped mark answers "is this cell in use right now?" Backtracking problems almost always want the second, and using the first is a quiet, common bug that produces false negatives.
The mechanism

Rather than allocating a separate marking structure, overwrite the cell with a character that cannot appear in the word, and put the original back on the way out.

python
def exist(board, word):
    rows, cols = len(board), len(board[0])

    def explore(r, c, i):
        if i == len(word):
            return True                       # whole word matched
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return False                      # off the board
        if board[r][c] != word[i]:
            return False                      # wrong letter, or '#' from this path

        original = board[r][c]
        board[r][c] = '#'                     # CHOOSE: claim for this path

        found = (explore(r + 1, c, i + 1) or
                 explore(r - 1, c, i + 1) or
                 explore(r, c + 1, i + 1) or
                 explore(r, c - 1, i + 1))

        board[r][c] = original                # UN-CHOOSE: always restore
        return found

    for r in range(rows):
        for c in range(cols):
            if explore(r, c, 0):
                return True
    return False
Crucial Notethe restore runs unconditionally, before returning, on the success path as well as the failure path. It is tempting to return true immediately from inside the direction checks — but that leaves '#' characters scattered across the caller's board. The function's contract is to answer a question, not to damage its input, and a caller running a second search on the same board would get wrong answers. Computing found first, restoring, then returning is what keeps the board pristine.
Crucial Notethe sentinel must be a character that cannot appear in word. Since the board and word are letters, '#' is safe. If arbitrary characters were possible, a separate visited set would be required — the in-place trick is an optimisation that depends on knowing the alphabet.
Crucial Notethe base case i == len(word) is tested before the bounds check, and that ordering matters. A word ending at a board edge would otherwise be rejected: the final character matches, the recursion advances to a neighbouring position that is off-grid, and if bounds were checked first it would return false despite the word being complete.
The cost

From each of the M × N starting cells, the search branches up to 4 ways at each of L steps, giving O(M × N × 4ᴸ).

The bound tightens slightly. After the first move, one of the four neighbours is the cell just left, which now holds '#' and fails the character test immediately. So the real branching is 3 after the first step, giving O(M × N × 3ᴸ).

That is a worst case rarely approached, because a mismatched character prunes a branch instantly — most paths die within two or three steps. A cheap improvement worth mentioning: skip any starting cell whose letter differs from the word's first character, avoiding a call that would fail on its first line.

Worked example:word = "ABCB"

Board:

text
A B C E
S F C S
A D E E

The interesting case is the one that fails, since it shows the marking doing its job.

- Start at (0,0), holding A, matching word[0]. Mark it '#' and recurse for word[1] = 'B'.
- Neighbours: (1,0) holds S, no match. (0,1) holds B — match. Mark it '#'. The board's top row now reads # # C E. Recurse for word[2] = 'C'.
- From (0,1): (1,1) holds F, no. (0,2) holds C — match. Mark it. Recurse for word[3] = 'B'.
- From (0,2): (1,2) holds C, no. (0,3) holds E, no. (0,1) holds '#', which does not equal 'B', so it is rejected — this is exactly the no-reuse rule firing. The B is physically there but belongs to this path already.
- Every direction fails. Restore (0,2) to 'C' and return false.
- Back at (0,1): no directions remain. Restore to 'B', return false.
- Back at (0,0): no other neighbour matches B. Restore to 'A', return false.
- The outer loop tries every remaining cell. None starts a successful trace. Return false.

Note the board is byte-for-byte identical to the input at the end — every mark was paired with a restore. Now contrast with "ABCCED": the same first three steps occur, but from (0,2) the neighbour (1,2) holds C matching word[3], and the trace continues through (2,2) and (2,1) to complete, returning true up the chain — with each level restoring its cell as the true propagates.

Where it sits in the section

This is the section's bridge from abstract enumeration to search over a concrete structure. The skeleton has not changed; what changed is that the shared state is the input itself rather than a list being built.

The termination style matches Sudoku rather than the enumeration problems. Compare:

- Collect every answer — return nothing, append to a shared list, always undo. Subsets, Combinations, Permutations, Combination Sum, N-Queens.
- Stop at the first answer — return a boolean, propagate success upward, undo on failure. Word Search, Sudoku.

Word Search takes the stricter line of restoring even on success, because the board belongs to the caller. Sudoku deliberately does not, because there the modified board is the answer. Knowing which applies is a matter of asking who owns the mutated state.

Recognition signals: path on a grid, trace a sequence through cells, route without revisiting, maze with a required pattern. The tell is a no-reuse rule scoped to a single path, which is what forces marks to be temporary.

The natural follow-up is searching for many words at once, which is Word Search II — running this function once per word is too slow, and the fix is to hold all the words in a trie and walk the board against it a single time.

Train the reflex: when a search modifies shared state, every modification needs a matching restore on the way out — and if the state belongs to the caller, that restore must happen on success too.

Interactive Strategy Visualization

Word Search (DFS)

Searching: ABCCED
A
B
C
C
E
D
A
B
C
E
S
F
C
S
A
D
E
E
Goal: Find if the word 'ABCCED' exists in the grid. We can move Up, Down, Left, Right.
MINDSET

We explore the grid like a graph. Every cell is a node, and every adjacent cell is an edge. We only traverse an edge if the character matches the next one in our target word.

BACKTRACKING

We must mark cells as 'visited' during a path to avoid cycles, but we MUST unmark them (backtrack) so they can be reused in different branches.

O(M × N × 4ᴸ) Four Directions At Every Step
O(M × N × 3ᴸ) Never Revisiting The Cell Just Left
O(M × N × 3ᴸ) With First-Letter Filtering