79. Word Search
On LeetCode ->Reformulated question¶
Given a 2D board of letters and a word, return whether you can trace the word by moving up, down, left, or right, using each cell at most once.
- Example:
board=[["A","B"],["C","D"]],word="ABD"->Truebecause path is(0,0)->(0,1)->(1,1)
Key trick¶
Use DFS backtracking from every cell.
- Match one character at a time.
- Mark the current cell as used during this path.
- Restore it when backtracking.
- Stop as soon as the full word is matched.
Trap¶
Common mistakes:
- Not tracking visited cells per path, so the same cell gets reused.
- Using a global visited set and forgetting to unmark on backtrack.
- Building and comparing whole path strings repeatedly, which is slower and unnecessary.
- Forgetting early return once a path succeeds.
- Mutating the board and not restoring it.
Why is this question interesting?¶
It is a clean backtracking problem with just enough state to expose whether you understand:
- DFS on grids
- per-path state
- pruning
- in-place marking vs extra memory
Pytest test¶
import pytest
@pytest.mark.parametrize(
("board", "word", "expected"),
[
(
[["A", "B", "C", "E"],
["S", "F", "C", "S"],
["A", "D", "E", "E"]],
"ABCCED",
True,
),
(
[["A", "B", "C", "E"],
["S", "F", "C", "S"],
["A", "D", "E", "E"]],
"SEE",
True,
),
(
[["A", "B", "C", "E"],
["S", "F", "C", "S"],
["A", "D", "E", "E"]],
"ABCB",
False,
),
(
[["A"]],
"A",
True,
),
(
[["A"]],
"B",
False,
),
(
[["A", "A"]],
"AAA",
False,
),
(
[["C", "A", "A"],
["A", "A", "A"],
["B", "C", "D"]],
"AAB",
True,
),
(
[["A", "B"],
["C", "D"]],
"ABCD",
False,
),
],
)
def test_exist(board, word, expected):
assert Solution().exist(board, word) is expected
Solve the problem with idiomatic python¶
class Solution:
def exist(self, board: list[list[str]], word: str) -> bool:
rows = len(board)
cols = len(board[0])
def dfs(r: int, c: int, i: int) -> bool:
# All characters matched.
if i == len(word):
return True
# Out of bounds or current cell does not match next needed char.
if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != word[i]:
return False
# Mark this cell as used for the current path.
ch = board[r][c]
board[r][c] = "#"
# Try all 4 directions for the next character.
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)
)
# Restore the cell for other paths.
board[r][c] = ch
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return False
- Time: \(O(mn \cdot 4^L)\) in the worst case, where \(L = len(word)\)
- Space: \(O(L)\) recursion stack
Comment my solution¶
Your solution has the right overall idea, but it misses the key visited-state rule.
Problems:
- It never marks cells as used, so the same cell can be revisited in one path.
pathstores letters only, not positions, so it cannot prevent reuse."" .join(path)is recomputed many times, which is avoidable overhead.- Using
foundis not needed; DFS can just returnTrueorFalse. - Checking
if foundbefore callingdfsin the outer loop does nothing useful.
A simpler shape is:
- DFS takes
(r, c, i)whereiis the index inword - Return boolean directly
- Mark/unmark the board cell during recursion
For example, this is the missing bug in spirit:
- Your code can reuse the first
"A"again, which is illegal.
## Solution
class Solution:
def exist(self, board: list[list[str]], word: str) -> bool:
m = len(board)
n = len(board[0])
path = []
# I don't like using found like this but I don't see how to do
# it differently here.
found = False
def dfs(r,c):
nonlocal found
if "".join(path) == word:
found = True
return
# path is not a prefix
if "".join(path) != word[:len(path)]:
return
for i, j in [[r - 1, c], [r + 1, c], [r, c - 1], [r, c + 1]]:
if i < 0 or i >= m or j < 0 or j >= n:
continue
path.append(board[i][j])
dfs(i,j)
path.pop()
for r in range(m):
for c in range(n):
path = [board[r][c]]
if found:
return True
else:
dfs(r,c)
return False
Solution().exist([["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], "ABCCED") # True
# The following one return True on LeetCode server, but on my machine
# it returns the correct answer False which is also what LeetCode test
# seems to expect.
Solution().exist([["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], "ABCD") # False