def subsets(nums: list[int]) -> list[list[int]]:
result = []
def backtrack(start: int, path: list[int]):
result.append(path[:]) # collect at every nodefor i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
Time: O(n · 2ⁿ) | Space: O(n)
关键点 / Key Points:
每个节点都是合法子集,在每次调用时都收集结果
start 参数防止重复,确保只向右扩展
path[:] 浅复制必须有,否则最终结果都是空列表
For the complete backtracking deep dive covering all 5 problem types (Subsets, Combination Sum, Permutations, Subsets II, N-Queens), see today's deep dive archive.
🗣️ 软技能 / Soft Skills — Day 88 · Expert Synthesis
主题 / Topic: Saturday Deep Dive Week — 回溯思维在工程决策中的应用
Situation: 6 weeks into a major architectural refactor, we discovered the new design couldn't meet our latency SLOs due to an unforeseen cross-region call pattern.
Task: I needed to make the call: push forward and try to optimize, or backtrack and rethink the foundation.
Action:
Called an emergency design review with the team — no blame, just diagnosis
Built a quick proof-of-concept to validate whether optimization was viable (it wasn't)
Documented what we'd learned from the 6 weeks — not wasted, it was discovery
Proposed a revised design incorporating the new constraints
Communicated transparently to stakeholders: "We found a better path, and here's why pivoting now saves us 3 months later"
Result: New design shipped 8 weeks later, 40% faster than the original would have been with patches. Team morale stayed high because I normalized backtracking as learning, not failure.
Saturday deep dive: today's deep dive archive covers backtracking algorithms from Subsets to N-Queens.
🤖 AI 概念 / AI Concept — Day 88 · Expert Synthesis
主题 / Topic: Saturday Deep Dive Week — Synthesis: Backtracking in AI/LLM Systems
AI 系统中的"回溯" / "Backtracking" in AI Systems
回溯算法的思想在现代 AI 系统中有深刻的对应,值得深入思考。
1. Beam Search — LLM 的结构化回溯
当 LLM 生成文本时,贪心解码(每步选最高概率 token)往往不是全局最优。Beam Search 维护 k 个候选序列(beams),类似回溯中同时探索多条路径:
贪心: "The cat sat" → 下一步选最高概率 → 可能进入死胡同
Beam Search (k=3):
Beam 1: "The cat sat on the mat" (score: 0.82)
Beam 2: "The cat sat quietly" (score: 0.79)
Beam 3: "The cat sat and purred" (score: 0.71)
这就是回溯的变体:不是深度优先,而是宽度优先地维护多条路径。
2. Tree of Thoughts — 显式回溯推理
Tree of Thoughts (ToT) 让 LLM 显式地探索推理树,并使用启发式函数来剪枝:
Backtracking is "controlled brute-force." You explore a decision tree depth-first, undo choices when they lead to dead ends, and continue. The key insight: you're building a result incrementally, and the "undo" step is what makes it backtracking (not just DFS).
This deep dive covers the universal template, 5 core problem types, and the mental model that lets you derive solutions from scratch in interviews.
The "undo" step is critical — it restores state before trying the next branch. Without undo, you'd need to copy state on every call (expensive). With undo, you reuse a single mutable path.
通用模板 / Universal Template
def solve(problem_input):
result = []
def backtrack(path, start_or_remaining):
# BASE CASE: path is completeif is_complete(path):
result.append(path[:]) # ← shallow copy! path will mutatereturn# EXPLORE choicesfor choice in get_choices(start_or_remaining):
# PRUNE: skip invalid choices earlyifnot is_valid(choice, path):
continue# CHOOSE
path.append(choice)
# RECURSE
backtrack(path, next_state(start_or_remaining, choice))
# UNDO (backtrack)
path.pop()
backtrack([], initial_state)
return result
五种变体的关键差异 / 5 Problem Types
问题类型
是否排列敏感
是否可重用元素
关键参数变化
子集 Subsets
否
否
start 递增
组合 Combination Sum
否
是
start 不变
排列 Permutations
是
否
used[] 标记
子集II (有重复)
否
否
排序 + 去重
N-Queens
是
否
cols/diags 标记
Part 2: Step-by-Step Implementation / 一步一步实现 (8 min)
Problem 1: Subsets (#78) — 基础模板
中文: 给定不含重复元素的整数数组 nums,返回所有可能的子集(幂集)。
English: Return all possible subsets (power set) of a list with no duplicates.
def subsets(nums: list[int]) -> list[list[int]]:
result = []
def backtrack(start: int, path: list[int]):
# Every node in the tree IS a valid subset (including empty)# So we collect at EVERY call, not just leaf nodes
result.append(path[:]) # copy current pathfor i in range(start, len(nums)):
# CHOOSE: add nums[i] to path
path.append(nums[i])
# RECURSE: only consider elements after i (no duplicates, no reuse)
backtrack(i + 1, path)
# UNDO: remove nums[i] from path
path.pop()
backtrack(0, [])
return result
# Example: nums = [1, 2, 3]# Decision tree:# []# / | \# [1] [2] [3]# / \ \# [1,2] [1,3] [2,3]# /# [1,2,3]# Result: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
复杂度 / Complexity: Time O(n · 2ⁿ), Space O(n) recursion stack + O(n · 2ⁿ) output
Problem 2: Combination Sum (#39) — 元素可重复使用
中文: 从 candidates 中选数,允许重复使用,找出所有和为 target 的组合。
English: Find all unique combinations that sum to target; each number may be used unlimited times.
def permute(nums: list[int]) -> list[list[int]]:
result = []
used = [False] * len(nums) # track which elements are in current pathdef backtrack(path: list[int]):
if len(path) == len(nums):
# COMPLETE: used all elements exactly once
result.append(path[:])
returnfor i in range(len(nums)):
if used[i]:
continue# PRUNE: already in path# CHOOSE
used[i] = True
path.append(nums[i])
# RECURSE: no start param — we consider ALL unused elements
backtrack(path)
# UNDO
path.pop()
used[i] = False
backtrack([])
return result
# KEY DIFFERENCE from subsets/combinations:# - No `start` parameter (order matters, can pick from anywhere)# - Uses `used[]` array instead to avoid duplicates within a path
Problem 4: Subsets II (#90) — 处理重复元素
中文: 数组中有重复元素,要求输出不重复的子集。
English: Array has duplicates; return unique subsets (no duplicate subsets).
def solveNQueens(n: int) -> list[list[str]]:
result = []
# Track which columns and diagonals are occupied
cols = set()
pos_diag = set() # row + col is constant along \ diagonals
neg_diag = set() # row - col is constant along / diagonals
board = [['.'for _ in range(n)] for _ in range(n)]
def backtrack(row: int):
if row == n:
# COMPLETE: placed queen in every row
result.append([''.join(r) for r in board])
returnfor col in range(n):
# PRUNE: this position is under attackif col in cols or (row + col) in pos_diag or (row - col) in neg_diag:
continue# CHOOSE: place queen
cols.add(col)
pos_diag.add(row + col)
neg_diag.add(row - col)
board[row][col] = 'Q'# RECURSE: move to next row
backtrack(row + 1)
# UNDO: remove queen
cols.remove(col)
pos_diag.remove(row + col)
neg_diag.remove(row - col)
board[row][col] = '.'
backtrack(0)
return result
# Key insight: one queen per row (we recurse row by row)# Diagonal detection:# pos_diag (↘): cells on same diagonal have equal row+col# neg_diag (↗): cells on same anti-diagonal have equal row-col
复杂度 / Complexity: Time O(n!), Space O(n²)
Cross-Check: Verify Diagram vs Code
Let's trace N-Queens with n=4, verifying the constraint sets work correctly:
Board state when placing queen at row=0, col=1:
cols = {1}
pos_diag = {0+1=1}
neg_diag = {0-1=-1}
board:
. Q . .
. . . .
. . . .
. . . .
Next call: row=1, try col=0:
col=0 not in cols={1} ✓
row+col=1+0=1 IN pos_diag={1} ✗ → PRUNE (correct! col0 row1 is on same ↘ diagonal)
Try col=2:
col=2 not in cols ✓
1+2=3 not in pos_diag={1} ✓
1-2=-1 IN neg_diag={-1} ✗ → PRUNE (correct! col2 row1 is on same ↗ diagonal)
Try col=3:
col=3 not in cols ✓
1+3=4 not in pos_diag ✓
1-3=-2 not in neg_diag ✓ → PLACE ✓
Diagram and code are consistent — the set-based constraint propagation correctly prunes all attacked positions.
Part 3: Edge Cases & Gotchas / 边界情况 (2 min)
1. 忘记复制路径 (Forgetting to copy path)
# WRONG — appends reference to same list
result.append(path) # ← all entries will be empty [] at the end!# CORRECT
result.append(path[:]) # shallow copy is fine for flat lists
result.append(list(path)) # equivalent
2. 重复问题不排序 (Skipping sort for duplicates)
Subsets II, Combination Sum II, Permutations II all require sorting first
Without sort, duplicates aren't adjacent → dedup logic breaks
3. 收集时机错误 (Wrong collection point)
Subsets: collect at EVERY call (every node is a result)
Combination Sum: collect only when remaining == 0
Permutations: collect only when len(path) == n
4. 组合 vs 排列的 start 参数
# Combination: pass i+1 to avoid reuse, i to allow reuse
backtrack(i + 1, ...) # no reuse
backtrack(i, ...) # allow reuse (Combination Sum)# Permutation: NO start param, use `used[]` instead
5. 提前终止 (Early termination)
# For sorted arrays, if current is already too large, prune the restfor i in range(start, len(candidates)):
if candidates[i] > remaining:
break# all remaining are larger (since sorted)
Part 4: Real-World Application / 实际应用 (2 min)
中文:
回溯在实际工程中比你想象的更常见:
正则表达式引擎 — .* 匹配时需要回溯尝试不同的匹配长度
编译器解析 — 语法树解析中的回溯文法
约束求解 — Sudoku solvers, scheduling systems, SAT solvers
游戏 AI — Chess, Go 的 minimax 树(带剪枝)
包依赖解析 — pip/npm 解析依赖版本组合
English:
Backtracking powers systems you use daily:
Regex engines: .* tries all possible lengths, backtracks on mismatch
SQL query planners: try different join orders, backtrack when cost estimate is poor
IDE autocomplete: generate candidate completions, prune invalid ones
Package managers: resolve dependency version combinations (a real combinatorial problem!)
The pattern: **explore → validate → backtrack** maps directly to any search-over-state-space problem.
Part 5: Interview Simulation / 面试模拟 (3 min)
面试官: Given Subsets, can you walk me through the decision tree?
你: Sure. We start with an empty path. For [1,2,3], at level 0 we choose whether to include 1: if we do, go left; if not, go right. Then same decision for 2, then 3. Every node (not just leaves) is a valid subset. That's why we collect at every recursive call, not just the base case.
面试官: How would you modify Subsets to handle duplicates like [1,2,2]?
你: Sort the array first, so duplicates are adjacent. Then during the for loop, if i > start and nums[i] == nums[i-1], we skip — this prevents generating duplicate subsets at the same tree level. The i > start condition is crucial: it only skips at the same level, not across levels (which would incorrectly prune [1,2] and [1,2] from different branches).
面试官: What's the time complexity of Permutations?
你: O(n · n!). There are n! permutations; for each, we spend O(n) copying the path into results. The recursion tree has at most n levels, each branching up to n times, but the used[] check ensures we don't revisit — so total nodes = n! (leaf) + n!/1 + n!/2 + ... ≈ O(n · n!).
面试官: When would you use backtracking vs dynamic programming?
你: DP when you need the optimal value of overlapping subproblems. Backtracking when you need to enumerate all valid solutions (or just one). If the problem says "find ALL combinations/permutations/subsets", that's backtracking. If it says "find the MINIMUM cost / MAXIMUM value", lean toward DP first.
面试官: Can you solve Combination Sum without backtracking? (follow-up)
你: You could use iterative BFS with a queue of (current_path, remaining), but it's much less clean and has the same complexity. Backtracking's elegance is in the natural undo via the call stack — the recursive structure mirrors the problem structure. I'd keep it as backtracking.
Summary: The Backtracking Cheat Sheet
CHOOSE what to add to path (loop over choices)
↓ PRUNE if invalid (i > start + duplicate, used[], constraint violated)
↓ RECURSE (pass new start OR same start for reuse)
↓ at base case: result.append(path[:])
↑ UNDO: pop from path, unmark used[]
Nine problems this block:
#78 Subsets ← today
#39 Combination Sum
#46 Permutations
#90 Subsets II
#40 Combination Sum II
#79 Word Search
#131 Palindrome Partitioning
#17 Letter Combinations of Phone Number
#51 N-Queens
Internalize the template once. The rest is parameter tuning.