25. Find Pattern in KB Grid
Problem
KB content gets indexed into a 2D grid of short tokens for a structured-content matching experiment. Given a target word, check whether it can be traced as a path of adjacent (up/down/left/right, no cell reused) cells that spell it out end to end — a toy stand-in for pattern matching over structured content.
Grid cells are single characters. Return True if the word can be traced as a path
of adjacent cells with no cell reused within the same path, and False otherwise.
Source: src/25_find_pattern_in_kb_grid.py
def exists(grid: list[list[str]], word: str) -> bool:
...
>>> grid = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]
>>> exists(grid, "ABCCED")
True
>>> exists(grid, "SEE")
True
>>> exists(grid, "ABCB")
False
Step-by-Step Approach
- Handle the trivial case: an empty
wordis trivially found (returnTrue). - Write a recursive
dfs(r, c, i)that asks: "can the remaining suffixword[i:]be traced starting from cell(r, c)?" - Base case for success: if
i == len(word), every character has already been matched, so returnTrue. - Base cases for failure: if
(r, c)is out of bounds, orgrid[r][c] != word[i](this cell doesn't hold the character we need next), returnFalse. - Otherwise the current cell matches. Temporarily mark it as visited (e.g. overwrite it with a
sentinel like
"#"that can't match any real letter) so the same path can't reuse it, then recursively try all four neighbors fori + 1. - After trying all four directions, restore the cell's original character before returning — this "undo" step is what makes it backtracking: the cell is only off-limits for paths that currently include it, not for paths explored later from a different start.
- Try starting the search from every cell in the grid; if any start succeeds, the word exists.
The key insight is the mark-then-unmark (backtrack) step: without restoring the cell after exploring it, a failed path would permanently corrupt the grid for every subsequent search attempt from other starting cells.
Reference solution
def exists(grid: list[list[str]], word: str) -> bool:
# backtracking DFS, marking visited cells in place; O(rows*cols*4^len(word)) worst case
if not word:
return True
rows, cols = len(grid), len(grid[0]) if grid else 0
# closure: reads grid/word/rows/cols from enclosing scope
def dfs(r: int, c: int, i: int) -> bool:
# success: every char of word already matched
if i == len(word):
return True
# bounds short-circuits before indexing
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != word[i]:
return False
saved = grid[r][c]
# mark visited in place, no separate visited set needed
grid[r][c] = "#"
# or short-circuits on first True
found = dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1) or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1)
# unmark/backtrack, run unconditionally
grid[r][c] = saved
return found
# try every start cell, short-circuit on first hit
return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))
Key Functions & Tricks
def dfs(r, c, i) -> bool:— nested closure, reuses enclosing scope'sgrid/word/rows/cols.i == len(word)— success base case: every character ofwordmatched.r < 0 or ... or grid[r][c] != word[i]— combined bounds + content guard, short-circuit safe.saved = grid[r][c]; grid[r][c] = "#"— mark step: sentinel excludes cell from reuse in this path.dfs(...) or dfs(...) or dfs(...) or dfs(...)— tries all four directions, short-circuits on firstTrue.grid[r][c] = saved— unmark/backtrack, unconditional, prevents permanently corrupting the grid.any(dfs(r, c, 0) for r in range(rows) for c in range(cols))— tries every start cell, short-circuits.
How to Recognize This Pattern
Signal words: "find a path through a grid/board spelling out a sequence," "no cell/element reused," "explore all possibilities and undo." Whenever a problem asks to explore a combinatorial space of paths or arrangements where each choice affects what's valid next (and wrong choices need to be undone to try others), that's backtracking — DFS plus a "mark before recursing, unmark after" step. Common variations: return all matching paths instead of just a boolean, require diagonal moves too, or search for multiple words simultaneously with a trie to share prefix work (the classic "Word Search II" follow-up). A common pitfall is forgetting to restore the visited marker after the recursive calls return — that silently makes cells permanently unusable across different starting points, producing false negatives on grids with multiple valid paths.