TL;DR
DFS backtracking with per-character pruning and in-place cell marking β O(mΒ·n Β· 3^L) time (L = len(word)), O(L) space.
Approach 1 β Brute force: enumerate full paths, compare at the end
The naive idea: from every cell, enumerate every simple path of exactly len(word) steps (carrying a visited set and the string built so far), and only when a path reaches full length compare it against word.
class Solution:
def exist(self, board: list[list[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
def paths(r: int, c: int, used: set[tuple[int, int]], acc: str) -> bool:
acc += board[r][c]
if len(acc) == len(word):
return acc == word # only compare once the path is complete
used = used | {(r, c)} # copies the set at every step
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in used:
if paths(nr, nc, used, acc):
return True
return False
return any(
paths(r, c, set(), "")
for r in range(rows)
for c in range(cols)
)
Complexity: O(mΒ·n Β· 3^L Β· L) time (every length-L path is walked and its string rebuilt, mismatch or not), O(LΒ²) space from copying used and acc per level.
The killer: it explores the entire 3^L-branching tree even when the very first character is wrong. With L = 15 thatβs millions of pointless paths per starting cell.
Approach 2 β Backtracking with early mismatch pruning and in-place marking
The insight: compare character by character as you walk. The moment board[r][c] != word[i], that whole subtree dies β nothing below it can spell the word. This is textbook backtracking: extend a partial solution one choice at a time, and undo the choice when the branch fails. For the βusedβ bookkeeping, temporarily overwrite the cell with "#" (which matches no letter) and restore it on the way out β the board itself becomes the visited set.
class Solution:
def exist(self, board: list[list[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
def dfs(r: int, c: int, i: int) -> bool:
if board[r][c] != word[i]:
return False # prune: mismatch kills the branch
if i == len(word) - 1:
return True # matched the last character
saved = board[r][c]
board[r][c] = "#" # mark in place
found = False
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and dfs(nr, nc, i + 1):
found = True
break
board[r][c] = saved # unmark (backtrack)
return found
return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))
Walkthrough on the example board with word = "SEE":
A B C E
S F C S
A D E E
- Starts at (0,0)
A, (0,1) B, (0,2) C, (0,3) E all fail instantly: first char must be S β one comparison each, no descent.
- (1,0)
S matches word[0]. Mark it #. Neighbors: (2,0) A, (0,0) A, (1,1) F β none is E; every child dies at depth 1. Restore S.
- Continue scanning; (1,3)
S matches. Mark #. Try (2,3) E = word[1]: mark #, try its neighbors for word[2] = "E": (1,3) is # (in use β sentinel canβt match), (2,2) is E β i == 2 == len(word)-1 β True bubbles all the way up.
Complexity: O(mΒ·n Β· 3^L) time β from each of mΒ·n starts, each path step has at most 3 unvisited directions (you never go back where you came from); O(L) recursion stack. No extra visited structure.
Approach 3 β Add feasibility pruning: letter counts + rarer-end start
The insight: two cheap global checks slash the worst cases. (1) If word needs more copies of some letter than the whole board contains, answer False without searching. (2) The search is cheapest when the first character is rare on the board β if the last character of word is rarer than the first, search for the reversed word instead (a path spelling the reverse exists iff one spelling the word does, walked backward).
from collections import Counter
class Solution:
def exist(self, board: list[list[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
board_count = Counter(ch for row in board for ch in row)
word_count = Counter(word)
if any(word_count[ch] > board_count[ch] for ch in word_count):
return False # board can't even supply the letters
if board_count[word[0]] > board_count[word[-1]]:
word = word[::-1] # anchor the search on the rarer end
def dfs(r: int, c: int, i: int) -> bool:
if board[r][c] != word[i]:
return False
if i == len(word) - 1:
return True
saved = board[r][c]
board[r][c] = "#"
found = False
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and dfs(nr, nc, i + 1):
found = True
break
board[r][c] = saved
return found
return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))
Walkthrough with word = "ABCB": the board contains exactly one B, but word needs two β the Counter check returns False immediately, zero DFS calls. (Approach 2 would have explored the A β B β C prefix before failing.)
Complexity: same worst-case O(mΒ·n Β· 3^L) time and O(L) space, but the pre-checks are O(mΒ·n + L) and routinely turn pathological inputs (e.g. a board of all As and word = "AAAAAAAAAAAAAAB") into instant rejections or far shallower searches.
Common pitfalls
- Forgetting to restore the cell after recursion β including on the success path if you return early before unmarking; corrupted boards break sibling starting positions.
- Using a shared visited set without removing entries on backtrack β cells stay βusedβ for unrelated branches and valid paths get missed.
- Checking
i == len(word) after moving instead of i == len(word) - 1 at the match β both can work, but mixing the two conventions yields off-by-one misses on single-character words.
- Allowing diagonal moves or revisits β the path is 4-directional and simple;
[["a"]] with word = "aa" must return False.
Pattern takeaway
Grid-path backtracking = DFS + fail fast + undoable state. Check the constraint (character match) before recursing so mismatched subtrees are never entered, encode βvisitedβ by mutating the structure itself with a sentinel and restoring it on unwind, and when the search has a choice of anchor, anchor it on the rarest element. The same skeleton solves island counting, path finding with restrictions, and any exists-a-path puzzle on a grid.