回溯算法¶
概述¶
回溯算法(Backtracking)是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化丢弃该解,即"回溯"并再次尝试。
核心思想:走不通就回头。本质是带剪枝的深度优先搜索(DFS),通过系统地搜索问题的解空间树,在搜索过程中用约束条件减去那些实际上不可能产生解的子树。
回溯算法本质¶
解空间树¶
回溯法将问题的解空间组织成一棵树,通过深度优先搜索遍历这棵树:
graph TB
subgraph 解空间树
root["根节点<br/>初始状态"]
l1a["选择1"]
l1b["选择2"]
l1c["选择3"]
l2a["1-1"]
l2b["1-2"]
l2c["2-1"]
l2d["2-2"]
l2e["3-1"]
l2f["3-2"]
root --> l1a --> l2a
l1a --> l2b
root --> l1b --> l2c
l1b --> l2d
root --> l1c --> l2e
l1c --> l2f
end
style root fill:#4CAF50,color:#fff
style l2a fill:#E8F5E9
style l2d fill:#FFEBEE
说明: - 绿色节点:满足约束,继续搜索 - 红色节点:不满足约束,剪枝回溯 - 白色节点:待探索
回溯三要素¶
| 要素 | 说明 | 示例 |
|---|---|---|
| 路径 | 已经做出的选择 | 已选的数字、已放置的皇后位置 |
| 选择列表 | 当前可以做的选择 | 剩余可选的数字、可放置的列 |
| 结束条件 | 达到决策树的叶节点 | 已选够k个数、所有皇后已放置 |
回溯框架¶
通用框架¶
| C | |
|---|---|
框架流程图¶
flowchart TD
A["进入 backtrack"]
B{"满足终止条件?"}
C["保存结果"]
D["return"]
E["遍历选择列表"]
F{"还有选择?"}
G["做选择"]
H["递归 backtrack"]
I["撤销选择"]
J["继续下一个选择"]
K["返回上一层"]
A --> B
B -->|是| C --> D
B -->|否| E --> F
F -->|是| G --> H --> I --> J --> F
F -->|否| K
style A fill:#E3F2FD
style C fill:#E8F5E9
style G fill:#FFF3E0
style I fill:#FFEBEE
关键点:撤销选择是回溯的核心,它保证了每次递归返回后,状态恢复到进入前的状态,使得同一层的选择不会相互影响。
经典问题¶
1. 全排列¶
问题:给定一个不含重复数字的数组,返回所有可能的全排列。
搜索树可视化:
graph TB
subgraph 全排列搜索树_nums_1_2_3
root(("[]"))
n1(("[1]"))
n2(("[2]"))
n3(("[3]"))
n4(("[1,2]"))
n5(("[1,3]"))
n6(("[2,1]"))
n7(("[2,3]"))
n8(("[3,1]"))
n9(("[3,2]"))
n10(("[1,2,3]"))
n11(("[1,3,2]"))
n12(("[2,1,3]"))
n13(("[2,3,1]"))
n14(("[3,1,2]"))
n15(("[3,2,1]"))
root --> n1 --> n4 --> n10
n1 --> n5 --> n11
root --> n2 --> n6 --> n12
n2 --> n7 --> n13
root --> n3 --> n8 --> n14
n3 --> n9 --> n15
end
style root fill:#4CAF50,color:#fff
style n10 fill:#E8F5E9
style n11 fill:#E8F5E9
style n12 fill:#E8F5E9
style n13 fill:#E8F5E9
style n14 fill:#E8F5E9
style n15 fill:#E8F5E9
实现:
执行过程示例(nums = [1,2,3]):
2. 组合¶
问题:给定两个整数 n 和 k,返回 1...n 中所有可能的 k 个数的组合。
搜索树可视化(n=4, k=2):
graph TB
subgraph 组合搜索树
root(("[]"))
n1(("[1]"))
n2(("[2]"))
n3(("[3]"))
n4(("[1,2]"))
n5(("[1,3]"))
n6(("[1,4]"))
n7(("[2,3]"))
n8(("[2,4]"))
n9(("[3,4]"))
root --> n1 --> n4
n1 --> n5
n1 --> n6
root --> n2 --> n7
n2 --> n8
root --> n3 --> n9
end
style root fill:#4CAF50,color:#fff
style n4 fill:#E8F5E9
style n5 fill:#E8F5E9
style n6 fill:#E8F5E9
style n7 fill:#E8F5E9
style n8 fill:#E8F5E9
style n9 fill:#E8F5E9
实现:
剪枝优化:当剩余可选元素不足以填满还需要的位置时,提前终止。循环上界从 n 改为 n - k + index + 1。
3. 子集¶
问题:给定一组不含重复元素的整数数组,返回该数组所有可能的子集。
搜索树可视化(nums = [1,2,3]):
graph TB
subgraph 子集搜索树
root(("[]"))
n1(("[1]"))
n2(("[2]"))
n3(("[3]"))
n4(("[1,2]"))
n5(("[1,3]"))
n6(("[2,3]"))
n7(("[1,2,3]"))
root --> n1 --> n4 --> n7
n1 --> n5
root --> n2 --> n6
root --> n3
end
style root fill:#E8F5E9
style n1 fill:#E8F5E9
style n2 fill:#E8F5E9
style n3 fill:#E8F5E9
style n4 fill:#E8F5E9
style n5 fill:#E8F5E9
style n6 fill:#E8F5E9
style n7 fill:#E8F5E9
实现:
4. N 皇后¶
问题:在 n×n 的棋盘上放置 n 个皇后,使其互不攻击。
约束条件: - 同一行不能有两个皇后 - 同一列不能有两个皇后 - 同一对角线不能有两个皇后
N=4 的搜索过程:
graph TB
subgraph N皇后搜索过程
r0(("第0行"))
r1a("Q在第0列")
r1b("Q在第1列")
r1c("Q在第2列")
r1d("Q在第3列")
r2a("✗ 无合法位置")
r2b("Q在2,3列")
r2c("✗ 无合法位置")
r2d("✗ 无合法位置")
r3a("继续搜索...")
r3b("找到解!")
r0 --> r1a --> r2a
r0 --> r1b --> r2b --> r3b
r0 --> r1c --> r2c
r0 --> r1d --> r2d
end
style r3b fill:#E8F5E9
style r2a fill:#FFEBEE
style r2c fill:#FFEBEE
style r2d fill:#FFEBEE
N=4 的一个解:
实现:
5. 数独求解¶
问题:填充 9×9 数独,使每行、每列、每个 3×3 宫格包含数字 1-9。
约束检查:
graph TB
subgraph 数独约束
A["单元格 row, col"]
B["检查同行 1-9"]
C["检查同列 1-9"]
D["检查3x3宫格 1-9"]
E["有效则放置"]
A --> B
A --> C
A --> D
B --> E
C --> E
D --> E
end
style E fill:#E8F5E9
实现:
6. 括号生成¶
问题:生成所有有效的括号组合(n 对括号)。
搜索树可视化(n=3):
graph TB
subgraph 括号生成搜索树
root((""))
n1(("("))
n2(("(("))
n3(("()"))
n4((("(((")))
n5((("(()")))
n6((("()(")))
n7(((")))"))
n8((("()()")))
n9((("((())))"))
n10((("(()())")))
n11((("()(())")))
n12((("()()()")))
root --> n1 --> n2 --> n4 --> n9
n2 --> n5 --> n10
n1 --> n3 --> n6 --> n11
n3 --> n8 --> n12
end
style n9 fill:#E8F5E9
style n10 fill:#E8F5E9
style n11 fill:#E8F5E9
style n12 fill:#E8F5E9
约束条件: - 左括号数量 ≤ n - 右括号数量 ≤ 左括号数量
实现:
7. 组合总和¶
问题:找出 candidates 中所有和为 target 的组合(可重复选择)。
实现:
8. 单词搜索¶
问题:在二维字符网格中搜索单词。
搜索过程可视化:
graph TB
subgraph 单词搜索_example
subgraph 网格
A["A"]
B["B"]
C["C"]
D["D"]
E["E"]
F["F"]
G["G"]
H["H"]
I["I"]
end
subgraph 搜索路径
S1["找到A"]
S2["向右找B"]
S3["向下找C"]
S4["找到!"]
end
S1 --> S2 --> S3 --> S4
end
style S4 fill:#E8F5E9
实现:
剪枝优化¶
1. 排序剪枝¶
对于组合问题,排序后可以提前终止:
2. 提前终止¶
只求一个解时,找到后立即返回:
3. 可行性剪枝¶
判断当前状态是否可能到达解:
| C | |
|---|---|
4. 对称性剪枝¶
利用问题的对称性减少搜索:
| C | |
|---|---|
剪枝效果对比¶
graph LR
subgraph 无剪枝
A1["搜索全部节点"]
B1["时间: O(n!)"]
end
subgraph 有剪枝
A2["大幅减少搜索空间"]
B2["时间: 大幅降低"]
end
A1 --> B1
A2 --> B2
style B1 fill:#FFEBEE
style B2 fill:#E8F5E9
时间复杂度分析¶
| 问题 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 全排列 | O(n × n!) | O(n) | n! 种排列,每种 n 个元素 |
| 组合 | O(C(n,k) × k) | O(k) | C(n,k) 种组合,每种 k 个元素 |
| 子集 | O(n × 2^n) | O(n) | 2^n 个子集,每个最多 n 个元素 |
| N皇后 | O(N!) | O(N) | 每行最多 N 个选择 |
| 括号生成 | O(4^n / √n) | O(n) | 第 n 个卡特兰数 |
| 数独 | O(9^(n×n)) | O(n²) | n×n 个格子,每个最多 9 种选择 |
注意:回溯算法本质是暴力搜索,时间复杂度通常是指数级。剪枝只能减少常数因子,不会改变渐进复杂度。对于大规模问题,需要考虑动态规划、贪心等更高效的算法。
回溯 vs 其他算法¶
| 特性 | 回溯 | 动态规划 | 贪心 | DFS/BFS |
|---|---|---|---|---|
| 目的 | 找所有解/最优解 | 找最优解 | 找局部最优解 | 遍历所有状态 |
| 状态恢复 | 需要撤销选择 | 不需要 | 不需要 | 不需要 |
| 剪枝 | 常用 | 不适用 | 不需要 | 较少使用 |
| 重叠子问题 | 不处理 | 利用 | 不处理 | 不处理 |
| 复杂度 | 指数级 | 多项式 | 多项式 | 线性/指数 |
C++ 模板实现¶
应用场景总结¶
| 应用场景 | 典型问题 | 关键技巧 |
|---|---|---|
| 排列组合 | 全排列、组合、子集 | 交换、顺序控制 |
| 约束满足 | N皇后、数独、图着色 | 约束检查、剪枝 |
| 路径搜索 | 迷宫、单词搜索 | 方向数组、访问标记 |
| 分割问题 | 字符串分割、分割回文串 | 子串枚举、有效性检查 |
| 括号问题 | 括号生成、有效括号 | 计数约束、平衡检查 |
| 组合优化 | 组合总和、目标和 | 排序剪枝、去重 |
实际问题示例¶
LeetCode 46. 全排列¶
| C++ | |
|---|---|
LeetCode 51. N皇后¶
设计模式¶
回溯问题设计步骤¶
flowchart TD
A["分析问题"]
B["确定解空间结构<br/>排列树/子集树/满N叉树"]
C["定义状态表示<br/>路径 + 选择列表"]
D["确定约束条件<br/>剪枝函数"]
E["实现回溯框架"]
F["添加剪枝优化"]
A --> B --> C --> D --> E --> F
style A fill:#E3F2FD
style F fill:#E8F5E9
设计要点:
1. 明确什么是"路径"、什么是"选择列表"
2. 确定递归的终止条件
3. 思考如何做选择和撤销选择
4. 分析约束条件,设计剪枝函数
5. 考虑是否需要去重、排序等预处理
参考资料¶
- 《算法导论》第35章:回溯法
- 《算法设计》第5章:回溯与分支限界
- 《挑战程序设计竞赛》搜索与剪枝
- Knuth, D. E. (2015). The Art of Computer Programming, Volume 4, Fascicle 5: Backtracking