🏗️ System Design — Synthesis (Expert Phase, Day 94)
Saturday — Deep Dive day. System Design section is in synthesis mode (all 60 curated topics exhausted).
Today's Synthesis Note
This Saturday's content is focused on the Word Search deep dive (algorithms). On synthesis Saturdays for System Design, reflect on cross-cutting architectural patterns covered so far:
Key systems covered (Days 1–93):
Distributed systems: Consistent Hashing, Raft consensus, CAP theorem
Storage: Key-Value Store, Time-Series DB, Distributed File System (HDFS)
Real-time: Chat (WhatsApp), Live Streaming (Twitch), Collaborative Editor (Google Docs)
Serving: CDN, API Gateway, Rate Limiter as Service
Data pipelines: Kafka-based event streaming, ELK logging
Synthesis question to ponder:
How does a real-world system like GitHub combine: distributed file storage (HDFS-like), content-addressable hashing (consistent hashing ideas), CDN (static asset delivery), real-time (PR review notifications), and geospatial-aware routing (anycast CDN)?
See the Saturday Deep Dive (./2026-07-18-deepdive.md) for today's main content.
🧮 Word Search (LeetCode #79) — Backtracking Deep Dive
Note: This is a Saturday. The full content for this topic is in the Saturday Deep Dive (./2026-07-18-deepdive.md). This file is a reference stub.
Quick Summary
Problem: Given an m×n board of characters, determine if a given word exists — formed from sequentially adjacent cells (horizontal or vertical), where each cell may only be used once.
Pattern: Backtracking — mark cell as visited ('#'), recurse, restore.
def exist(board, word):
rows, cols = len(board), len(board[0])
def dfs(r, c, idx):
if idx == len(word): returnTrueif r < 0or r >= rows or c < 0or c >= cols: returnFalseif board[r][c] != word[idx]: returnFalse
temp, board[r][c] = board[r][c], '#'
found = (dfs(r+1,c,idx+1) or dfs(r-1,c,idx+1) or
dfs(r,c+1,idx+1) or dfs(r,c-1,idx+1))
board[r][c] = temp
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0): returnTruereturnFalse
Complexity: Time O(m×n×4^L), Space O(L) (call stack)
Landscape: Open vs closed source, AGI debates, multimodal, diffusion, coding assistants
Ops: Evaluation, monitoring, guardrails, cost management, context windows
Cross-cutting connection to today's algorithm topic:
Backtracking + pruning is directly analogous to how LLM agents explore tool-call plans. When an agent uses ReAct-style reasoning, each "thought → action → observation" is a DFS choice; failing branches get pruned. Word Search's frequency pre-check maps onto how well-designed agents refuse impossible tasks up front instead of wasting inference tokens.
Synthesis question to ponder:
How would you design an LLM-driven "structured search agent" that decides when to backtrack vs commit? What signal would tell it a branch is dead-end?
See the Saturday Deep Dive (./2026-07-18-deepdive.md) for today's main content.
Attention Heatmap
When the model processes it, it attends most strongly to animal.
The
animal
didnt
cross
the
street
because
it
was
too
tired
it →
🔬 Saturday Deep Dive: 回溯算法与二维网格搜索 — Word Search & Beyond (18 min read)
今天我们以 Word Search(LC #79) 为核心,深挖回溯在二维网格中的完整实现,然后拓展到:
为什么朴素 DFS 会超时,如何剪枝
访问标记的 3 种写法(in-place vs visited set vs bitmask)
Word Search II(多词同时搜索)如何用 Trie 优化
N-Queens 如何把同一模板推向 Hard 题
真实生产场景中的回溯思想
English:
Backtracking is one of the highest-frequency algorithm patterns in tech interviews. The core idea fits in one line: **choose → recurse → undo**. But what separates candidates is knowing how to prune, how to adapt the template, and how to reason about complexity.
We'll use Word Search (LC #79) as our anchor problem, then expand outward to pruning strategies, Trie-based multi-word search, N-Queens, and real production use cases.
Backtracking explores an implicit decision tree. At each node, you make a choice, recurse deeper, then undo that choice before trying the next option. The power comes from pruning: if the current partial state can never lead to a valid solution, skip the entire subtree.
Three mental models that help:
State space tree: visualize all possible choices as a tree
Constraint propagation: check constraints early, not at the leaf
Exact undo: match every mutation with its precise reversal
Word Search 问题定义
Grid:
A B C E
S F C S
A D E E
word = "ABCCED" → True
word = "SEE" → True
word = "ABCB" → False (can't reuse cells)
搜索规则:
从任意格子出发
每步只能走上下左右(4方向)
同一路径中不能重复使用同一格子
找到一条路径使得连起来的字母等于 word 即返回 True
Part 2: Step-by-Step Implementation / 一步一步实现 (8 min)
方案一:朴素回溯(Naive Backtracking)
中文:先写能跑的,再优化。
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, idx: int) -> bool:
# Base case: matched all charactersif idx == len(word):
returnTrue# Boundary + character check — prune early!if (r < 0or r >= rows or
c < 0or c >= cols or
board[r][c] != word[idx]):
returnFalse# Mark as visited by temporarily overwriting# This is the "in-place" trick: no extra set needed
temp = board[r][c]
board[r][c] = '#'# 标记已访问 / mark visited# Explore all 4 directions
found = (
dfs(r + 1, c, idx + 1) or
dfs(r - 1, c, idx + 1) or
dfs(r, c + 1, idx + 1) or
dfs(r, c - 1, idx + 1)
)
# Undo the mark — this is the "backtrack" step
board[r][c] = temp # 撤销标记 / restorereturn found
# Try every cell as the starting pointfor r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
returnTruereturnFalse
复杂度 / Complexity:
Time: O(M × N × 4^L) — M×N starting cells, each path up to 4^L branches
Space: O(L) — recursion stack depth equals word length
L is len(word). For a 6×6 board and 10-char word, this is 36 × 4^10 ≈ 36M ops — feasible.
方案二:关键剪枝优化
中文:4 个剪枝,每一个都有实际效果。
class Solution:
def exist(self, board: list[list[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
# ============================================================# PRUNING #1: Frequency check — O(M*N + L), saves huge time# 如果 board 里某个字母的数量 < word 里需要的数量,直接返回 False# ============================================================from collections import Counter
board_count = Counter(c for row in board for c in row)
word_count = Counter(word)
for ch, cnt in word_count.items():
if board_count[ch] < cnt:
returnFalse# ============================================================# PRUNING #2: Reverse word if tail is rarer than head# 从频率更低的一端开始搜索 → 更快剪枝# Example: word="AAAAAB" → start from 'B' side# ============================================================if board_count[word[0]] > board_count[word[-1]]:
word = word[::-1]
def dfs(r: int, c: int, idx: int) -> bool:
if idx == len(word):
returnTrueif (r < 0or r >= rows or
c < 0or c >= cols or
board[r][c] != word[idx]):
returnFalse
temp = board[r][c]
board[r][c] = '#'# ============================================================# PRUNING #3: Short-circuit with 'or' — stops at first True# Python 'or' short-circuits: once one branch returns True,# the remaining branches are not evaluated# ============================================================
found = (
dfs(r + 1, c, idx + 1) or
dfs(r - 1, c, idx + 1) or
dfs(r, c + 1, idx + 1) or
dfs(r, c - 1, idx + 1)
)
board[r][c] = temp
return found
for r in range(rows):
for c in range(cols):
# ============================================================# PRUNING #4: Skip starting cells that don't match word[0]# (implicit in dfs check, but can also be explicit)# ============================================================if dfs(r, c, 0):
returnTruereturnFalse
方案三:3 种访问标记写法对比
中文:in-place 改写最省空间,但需要保证撤销正确。
# Option A: In-place overwrite (推荐 / Recommended)# Pros: O(1) extra space, clean# Cons: board is mutated during search (not thread-safe)
temp = board[r][c]
board[r][c] = '#'# ... recurse ...
board[r][c] = temp
# Option B: visited set# Pros: board unchanged, intuitive# Cons: O(M*N) extra space, set add/remove overhead
visited = set()
visited.add((r, c))
# ... recurse ...
visited.remove((r, c))
# Option C: bitmask (for very small grids, e.g., 4x4)# Pros: O(1) space, fast bitwise ops# Cons: only works when M*N ≤ 64
mask |= (1 << (r * cols + c))
# ... recurse ...
mask ^= (1 << (r * cols + c)) # XOR to unset
面试中:用 in-place overwrite,解释清楚即可。
Word Search II — Trie 优化多词搜索
中文:如果要同时搜索 words 列表中的所有词,朴素方法是对每个词跑一遍 DFS = O(W × M × N × 4^L)。用 Trie 可以把 W 个词的搜索合并成一次 DFS。
class TrieNode:
def __init__(self):
self.children: dict[str, 'TrieNode'] = {}
self.word: str | None = None# store complete word at leafclass Solution:
def findWords(self, board: list[list[str]], words: list[str]) -> list[str]:
# Build Trie from all words
root = TrieNode()
for w in words:
node = root
for ch in w:
node = node.children.setdefault(ch, TrieNode())
node.word = w # mark end of word
rows, cols = len(board), len(board[0])
result = []
def dfs(r: int, c: int, node: TrieNode) -> None:
ch = board[r][c]
# KEY: check if this character exists in Trie# If not, prune entire subtree — this is the Trie speedupif ch notin node.children:
return
next_node = node.children[ch]
# Found a complete wordif next_node.word:
result.append(next_node.word)
next_node.word = None# de-duplicate: don't add same word twice# Mark visited
board[r][c] = '#'for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
nr, nc = r + dr, c + dc
if0 <= nr < rows and0 <= nc < cols and board[nr][nc] != '#':
dfs(nr, nc, next_node)
board[r][c] = ch # restore# Optimization: prune empty Trie nodes (leaf pruning)# If next_node has no more children and no word,# remove it from parent to speed up future searchesifnot next_node.children andnot next_node.word:
del node.children[ch]
for r in range(rows):
for c in range(cols):
dfs(r, c, root)
return result
Trie 优化效果:
Without Trie: O(W × M × N × 4^L) — one full search per word
With Trie: O(M × N × 4^L_max) — one search for all words simultaneously
关键:Trie 的前缀共享让多个词的公共前缀只走一次
N-Queens — 回溯模板的 Hard 变体
中文:N-Queens 是同一个回溯模板,但"约束检查"更复杂。
class Solution:
def solveNQueens(self, n: int) -> list[list[str]]:
result = []
# Track which columns, diagonals are under attack# 用 set 追踪哪些列、对角线、反对角线已被攻击
cols = set()
diag = set() # r - c is constant along a diagonal
anti = set() # r + c is constant along anti-diagonal
board = [['.'] * n for _ in range(n)]
def backtrack(row: int) -> None:
# Base case: placed queens in all n rowsif row == n:
result.append([''.join(r) for r in board])
returnfor col in range(n):
# Constraint check — prune invalid placementsif col in cols or (row - col) in diag or (row + col) in anti:
continue# Choose
cols.add(col)
diag.add(row - col)
anti.add(row + col)
board[row][col] = 'Q'# Recurse
backtrack(row + 1)
# Undo
cols.discard(col)
diag.discard(row - col)
anti.discard(row + col)
board[row][col] = '.'
backtrack(0)
return result
对角线技巧 / Diagonal Trick:
For any cell (r, c):
- Same diagonal: r - c = constant
- Same anti-diagonal: r + c = constant
Example (4x4):
r-c diagonals: r+c anti-diagonals:
-3 -2 -1 0 0 1 2 3
-2 -1 0 1 1 2 3 4
-1 0 1 2 2 3 4 5
0 1 2 3 3 4 5 6
Part 3: Edge Cases & Gotchas / 边界情况 (2 min)
中文 & English:
# ❌ Gotcha 1: Forgetting to restore board[r][c]# 最常见错误:忘记撤销 in-place 标记# If you mark '#' but don't restore, later DFS branches see '#'
board[r][c] = '#'
result = dfs(...)
# MUST restore before returning!
board[r][c] = temp # ← don't forget this# ❌ Gotcha 2: Off-by-one in base case# idx == len(word) means we matched ALL characters# Some people write idx == len(word) - 1 → misses last charif idx == len(word): # ✅ correctreturnTrueif idx == len(word) - 1and board[r][c] == word[idx]: # ✅ also correctreturnTrue# ❌ Gotcha 3: Word Search II — duplicate results# If the same word can be found via multiple paths,# de-duplicate by clearing node.word after first findif next_node.word:
result.append(next_node.word)
next_node.word = None# ← prevent duplicates# ❌ Gotcha 4: Empty board / single cell
board = [["a"]]
word = "a"# → True (idx hits len(word) immediately after matching)
word = "aa"# → False (can't reuse cell, no neighbors)# ❌ Gotcha 5: N-Queens — using list for cols check# O(n) lookup per placement → O(n^3) total# Use set for O(1) lookup → O(n^2) total
A: In-place saves O(M×N) space and avoids hash overhead. Trade-off: board is temporarily mutated — requires precise undo. Use visited set if board must remain immutable (e.g., multi-threaded search).
Q3: "如何优化 Word Search II 到让 1000 个词同时搜索也不超时?"
1. 用 Trie 合并所有词的搜索 → 从 O(W × M × N × 4^L) 降到 O(M × N × 4^L_max)
2. 找到一个词后立即从 Trie 中删除(leaf pruning)→ 避免重复找已找到的词
3. 频率剪枝:board 里没有某字母就不加入 Trie
A: Trie merges all word searches into one DFS pass. Add leaf pruning (delete matched word's node) to prevent re-visiting already-found words. Frequency pre-filter eliminates words with impossible characters.
Q4: "回溯和动态规划有什么区别?什么时候用哪个?"
回溯:需要所有解,或者问题有路径约束(不能复用元素),用回溯。
DP:只需要最优解或计数,子问题有重叠,用 DP。
本质区别:DP 记忆化子问题结果;回溯不记忆,靠剪枝。
A: Use backtracking when you need all solutions or have path constraints (can't reuse elements). Use DP when you need the optimal/count and subproblems overlap. Backtracking explores; DP remembers.
Q5: "N-Queens 的 r-c 对角线技巧能解释一下吗?"
在棋盘上,同一对角线(左上到右下)上的所有格子,row - col 值相同。同一反对角线上所有格子,row + col 值相同。所以用两个 set 分别存 r-c 和 r+c,O(1) 判断是否有冲突,比扫描整个 board O(n) 快得多。
A: On any diagonal (top-left to bottom-right), all cells share the same r - c value. On any anti-diagonal, all cells share r + c. Store these in sets for O(1) conflict checks instead of scanning the board each time.
🔗 Resources / 参考资料
🔗 LeetCode #79 Word Search: https://leetcode.com/problems/word-search/
📹 NeetCode Word Search: https://www.youtube.com/watch?v=pfiQ_PS1g8E
🔗 LeetCode #212 Word Search II: https://leetcode.com/problems/word-search-ii/
📹 NeetCode Word Search II: https://www.youtube.com/watch?v=asbcE9mZz_U