Begin Algo
Recursion & Backtracking · 05 / 05

Word SearchBacktracking on a grid

DFS across a grid, restoring the mark on the way back.

Used for: Word games, enumerating maze routes

Time complexityO(m·n·4ᴸ)
Space complexityO(L)
DifficultyIntermediate
PrerequisitesDFS, Matrix

01Why it exists

Validating an answer in a word game

A game like Boggle deals a board of letters, a player submits a word, and the system has to decide whether that word can be spelled out through adjacent cells, using each cell at most once.

Why this fitsStart from every cell that carries the first letter, extend up, down, left and right to match the next letter, and mark each cell as you step onto it. When a route dead-ends, clear the mark and try another. That is backtracking on a grid, and clearing the mark is what lets a different route pass through the same cell.

Listing every way through a maze

A level designer wants to know how many routes lead from the entrance to the exit without revisiting a cell, to judge whether the level is too easy.

Why this fitsBFS only ever finds the shortest one. Listing all of them takes DFS with backtracking: mark each cell as you enter it, record a path whenever you reach the goal, and clear the mark on the way back out. Leave the marks in place and the second route is never found.

Feasible action sequences for a robot arm

An arm has to move from its starting pose to a target through a series of actions, each step choosing one of four actions, with some intermediate poses forbidden. The task is to list every legal sequence of actions.

Why this fitsPoses are cells, actions are the four directions, and forbidden poses are walls — the problem has exactly the shape of grid backtracking. A grid is just the easiest state space to draw; the same code transfers to any problem that is states plus transitions.

Reach for it when you see:Grids, adjacent cells, each cell usable once, finding one path or every path, marking what you have visited, the four directions, turning back when a route dead-ends.

02The core idea

Grid backtracking turns the "choice" of backtracking into which direction to step. Starting from one cell, check whether its letter is word[i]; if it is, look for word[i+1] in the four neighbouring cells, and if any direction succeeds the whole search succeeds. This is the same thing as DFS on a graph, and the difference lies in how visited is handled: DFS looking for connected components never re-enters a cell once it has been visited, whereas grid backtracking has to clear the mark on the way back, because one cell may appear in several different paths — it just cannot appear twice in the same path.

The cheapest way to mark is to edit the board itself: rewrite the letter as # on entry and put it back on exit. Since # never equals a letter, the "already visited" test and the "wrong letter" test collapse into the single line board[r][c] != word[i]. If you would rather not touch the input, keep a separate visited array; the logic is identical.

The three steps are still make the choice (mark), recurse (four directions) and undo the choice (clear the mark). The pruning here is "return immediately if the letter is wrong", and since the test happens the moment you enter a cell, a failing branch never expands its four directions at all. Two more speedups are common: count the letters on the board up front and return false straight away if the board cannot supply what the word needs; and if the word's first letter is more common on the board than its last, search the word backwards, which leaves far fewer starting cells.

Complexity: m×n starting cells, each going at most L levels deep (L being the word's length), with 4 directions per level (3 if you never step straight back), giving an upper bound of O(m·n·4ᴸ). Space is the recursion depth, O(L). To find many words on the same board (Word Search II), do not search them one at a time: put every word into a trie and walk the trie alongside the DFS, so one search matches all the words at once.

03The algorithm

  1. 1Call dfs(r, c, 0) for every cell (r, c) of the board; if any of them returns true, the word is there.
  2. 2Prune first inside dfs(r, c, i): return false if the position is out of bounds or board[r][c] != word[i].
  3. 3If i == len(word) - 1, the last letter matched too, so return true.
  4. 4Make the choice: set board[r][c] to #, recurse with dfs(nr, nc, i + 1) into the neighbours above, right, below and left, and return true as soon as one of them does.
  5. 5Undo the choice: whatever the outcome, restore the original letter to board[r][c] before leaving. Restore it on success too — do not leave the board dirty.

04Interactive demo

Searching a 3×4 grid for "SEE". Starting cells are scanned from the top-left, and every S branches out in four directions. Watch the marks get cleared after all three directions out of the first S fail, and then, starting from the second S, watch the upper E dead-end and only the lower E succeed once the mark has been restored.

StartFind "SEE" · direction order: up, right, down, left
A
B
C
E
S
F
C
S
A
D
E
E
word (i = 0)
SEE
Current path
empty

A solid cell is a marked cell on the path (the small number is its position in the path), amber is the neighbour being probed, and a dashed border marks the cell whose mark was just undone.

Step 0/16Find "SEE" in a 3×4 grid. Every cell is a possible starting point; from a start, a DFS explores all four directions, marking each cell it walks through so that the same cell is never reused.

05Code

Word Search with in-place marking, plus the variant that lists every path through a maze (using a visited array, and recording rather than returning when it reaches the goal, so the search continues).

# Word Search (LeetCode 79): DFS on a grid. A visited cell is temporarily rewritten to '#' and restored on the way back
def exist(board, word):
    rows, cols = len(board), len(board[0])

    def dfs(r, c, i):
        if board[r][c] != word[i]:            # wrong letter in this cell
            return False
        if i == len(word) - 1:                # the last letter matched too
            return True
        ch = board[r][c]
        board[r][c] = "#"                     # make the choice: mark the cell as visited
        for dr, dc in ((-1, 0), (0, 1), (1, 0), (0, -1)):
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and dfs(nr, nc, i + 1):
                board[r][c] = ch              # restore even on success, do not leave the board dirty
                return True
        board[r][c] = ch                      # undo the choice: clear the mark
        return False

    return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))


# Variant: list every path through a maze from start to goal (0 is open, 1 is a wall)
def all_paths(maze, start, goal):
    rows, cols = len(maze), len(maze[0])
    ans = []
    path = []
    visited = [[False] * cols for _ in range(rows)]

    def dfs(r, c):
        path.append((r, c))
        visited[r][c] = True
        if (r, c) == goal:
            ans.append(path[:])
        else:
            for dr, dc in ((-1, 0), (0, 1), (1, 0), (0, -1)):
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and maze[nr][nc] == 0 and not visited[nr][nc]:
                    dfs(nr, nc)
        visited[r][c] = False                 # clear the mark: another path may still pass through this cell
        path.pop()

    dfs(*start)
    return ans


if __name__ == "__main__":
    board = [list("ABCE"), list("SFCS"), list("ADEE")]
    print(exist(board, "SEE"), exist(board, "ABCCED"), exist(board, "ABCB"))   # True True False
    maze = [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
    print(len(all_paths(maze, (0, 0), (2, 2))))                              # 2

06Practice

  • LeetCode 79Word SearchMedium
  • LeetCode 1219Path with Maximum Gold (every finished path has to clear its marks)Medium
  • LeetCode 130Surrounded Regions (DFS without clearing the marks — compare the difference)Medium
  • LeetCode 212Word Search II (paired with a trie)Hard
  • LeetCode 980Unique Paths III (list every path that covers all the empty cells)Hard
  • LeetCode 2328Number of Increasing Paths in a Grid (strictly increasing means no backtracking, so drop the marks and memoise instead)Hard