回溯法(Backtracking)
1. 核心思想
回溯法是一种系统地搜索问题解空间的方法,本质是带剪枝的深度优先搜索。它从根结点出发,按深度优先策略搜索解空间树;搜索到某个结点时,先判断该结点是否可能包含问题的解:
- 可能包含 → 进入该子树继续搜索;
- 一定不包含 → 跳过该子树(剪枝),回退到最近的活结点继续搜索。
若用回溯法求问题的所有解,要回溯到根,且根结点的所有可行子树都要被搜索到;若只求任一解,只要搜索到问题的一个解就可以结束。
2. 三个关键概念
| 概念 | 含义 |
|---|---|
| 解空间树 | 问题的所有可能解构成的树形结构,常见子集树与排列树 |
| 约束函数(constraint) | 剪掉不满足约束条件(不可行)的子树 |
| 限界函数(bound) | 剪掉虽可行但不可能优于当前最优解的子树 |
两类典型解空间树:
- 子集树:每个元素只有「选 / 不选」两种状态,共 $2^n$ 个叶子结点。典型问题:0-1 背包、装载问题、图着色。
- 排列树:确定 $n$ 个元素的排列顺序,共 $n!$ 个叶子结点。典型问题:旅行商问题(TSP)、批处理作业调度、n 皇后。
3. 算法框架
递归回溯(子集树)
void backtrack(int t) {
if (t > n) { // 到达叶子结点,得到一个可行解
output(x);
} else {
for (int i = 0; i <= 1; i++) { // 每个结点的分支数
x[t] = i;
if (constraint(t) && bound(t)) { // 剪枝
backtrack(t + 1);
}
}
}
}
迭代回溯(非递归)
void iterativeBacktrack() {
int t = 1;
while (t > 0) {
if (t > n) {
output(x);
t--; // 回溯
} else {
x[t]++; // 进入下一个分支
if (x[t] <= m && constraint(t) && bound(t)) {
t++; // 深入下一层
} else {
t--; // 本层分支枚举完,回溯
}
}
}
}
4. 复杂度
回溯法的时间复杂度取决于解空间树的大小与剪枝效果:
- 子集树:最坏 $O(2^n)$,每结点耗时 $O(1)$,故为 $O(n \cdot 2^n)$;
- 排列树:最坏 $O(n!)$,每结点耗时 $O(1)$,故为 $O(n \cdot n!)$。
剪枝函数写得越好,实际搜索的结点数越少,但最坏情况下的界不变。
5. 典型问题
| 问题 | 解空间树 | 约束 / 限界函数 |
|---|---|---|
| n 皇后 | 排列树 | 约束:不在同一列、同一对角线 |
| 0-1 背包 | 子集树 | 约束:不超重;限界:上界函数(当前价值 + 剩余物品价值上界) |
| 图着色 | 子集树 | 约束:相邻顶点颜色不同 |
| 旅行商问题 | 排列树 | 约束:路径存在;限界:已走路径长度 < 当前最优 |
| 装载问题 | 子集树 | 约束:不超过载重量 |
| 符号三角形 | 子集树 | 约束:+ 与 - 的个数不超过总数一半 |
6. 回溯法 vs 分支限界法
| 对比项 | 回溯法 | 分支限界法 |
|---|---|---|
| 搜索方式 | 深度优先 | 广度优先 / 优先队列(最小耗费优先) |
| 存储结构 | 栈(或递归栈) | 队列 / 优先队列 |
| 目标 | 找出所有(或任一)可行解 | 尽快找出最优解 |
| 剪枝方式 | 约束函数 + 限界函数 | 限界函数(上界/下界比较) |
评论