0 · 子集树 vs 排列树 & DFS 回溯 vs BFS/优先队列分支限界
- 两种解空间树的规模直觉:
① 子集树(0-1 选择,如 0-1 背包、装载、最大团):共 \(n\) 层,每个元素选或不选(二叉树),叶子数 \(2^n\),时间复杂度上界 \(O(2^n)\) 或 \(O(n2^n)\);
② 排列树(全排列顺序,如 TSP 旅行商、批处理作业调度):第 \(k\) 层从剩余 \(n-k+1\) 个元素中选一个,叶子数 \(n!\),时间复杂度上界 \(O(n!)\)。 - 剪枝函数的双刃剑:约束函数(Constraint)剪掉违反题目硬约束的左分支(如超重 \(\text{cw}+w_i > c\));限界函数(Bound)用贪心松弛估算子树能达到的理论最优上界 \(\text{best}\),若连理论上限都比不过当前已找到的最优解 \(\text{bestp}\),直接剪掉右分支!
自学导航 · 回溯与分支限界:真正理解剪枝
① 前置知识与记号
前置复习:递归调用栈、解空间树、组合与排列。约束剪枝拒绝不可行解;限界剪枝拒绝不可能改善当前最优值的分支。每次剪枝必须能解释其安全性。
② 完整例题
四皇后从第 0 行开始,每行放一个;记录已使用列、主对角线 r−c、副对角线 r+c。合法列序列 [1,3,0,2] 可作为手算答案。写清递归入口、冲突检测、撤销选择三步。
💡 破题关键与第一步提示(遮答案自测用 · ③ 考前自测(展开题目))
破题切入点:分支限界在最大化问题中为什么需要乐观上界?(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(③ 考前自测(展开题目))
分支限界在最大化问题中为什么需要乐观上界?何时可以安全剪掉分支?
本栏是补充学习材料,考试范围以本学期教师正式通知为准。
考试演算:子集和的搜索树与剪枝
题目:正整数数组 \([2,3,5,7]\),找所有和为 10 的子集。
- 状态:当前位置 \(i\)、已选和 \(s\)、当前选集。每层两个分支:取/不取 \(a_i\)。
- 约束剪枝:当 \(s>10\) 时,因为剩余全是正数,不可能回落到 10;若 \(s+\) 全部剩余元素之和 \(<10\),也不可能凑够 10。
- 深度搜索并在 \(s=10\) 时记录解,得到 \([2,3,5]\) 和 \([3,7]\)。
- 最坏需要检查 \(2^n\) 个取舍向量,空间深度 \(O(n)\)(不含输出全部解所需空间)。剪枝改善实例耗时,但通常不改变指数级最坏上界。
💡 破题关键与第一步提示(遮答案自测用 · 自己做:为何负数出现时不能使用「当前和超目标」剪枝?)
破题切入点:因为后续负数可能将和减小,例如目标 10、当前和 12、剩余元素 −2,仍能得到解。(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(自己做:为何负数出现时不能使用「当前和超目标」剪枝?)
因为后续负数可能将和减小,例如目标 10、当前和 12、剩余元素 −2,仍能得到解。剪枝必须依据题目约束证明,不可机械套用模板。
与分支限界区别:最大化问题的“乐观上界”若不超过已知可行解值,则该分支可以剪掉;保证正确性的逻辑与仅判断可行性不同。
先看:本模块易错点与补充
纠错与边界- 回溯既可找所有可行解,也可找到一个解或最优解,不应把“寻找所有解”当作必要定义。
- “第一艘船尽量装满”只给出第一艘可行的最大装载量;第二艘能否装完剩余箱,还需总重量不超过两船容量和及二船装载检验。
- Python n 皇后搜索中用列与两条对角线去重,每层回溯都要恢复状态。
0 · 回溯思想与三结点
- 扩展结点:正在产生儿子的结点;活结点:自身已生成但儿子未全部生成;死结点:儿子已全部生成。
- 显式约束:对分量取值范围的限定;隐式约束:为构成解而对不同分量间施加的约束(如皇后不同列、不同斜线)。
- 剪枝函数:约束函数剪去不可行子树 + 限界函数剪去得不到最优解的子树。回溯=DFS+剪枝;分支限界=BFS+剪枝(判断题!)。
- 空间复杂度:回溯只存根到当前结点的路径,\(O(h(n))\)(h 为树高)——显式存整个解空间要 \(O(2^{h(n)})\)。
- 算法填空——Review 原题风格:回溯递归框架挖空(边界判断 / 扩展 / 剪枝 / 递归调用 / 撤销),框架要默到肌肉记忆;
- 解空间树计数——子集树共 2ⁿ 个结点(n 件物品)、排列树 n! 个叶子(填空秒答);
- 回溯 vs 分支限界三区别——简答必背(求解目标 / 搜索方式 / 节点选择策略);
- 算法设计——TSP / 装载 / n 皇后 / 0-1 背包回溯版:写清解空间类型、剪枝函数(约束 + 限界)、搜索过程;
- 本章末尾的 P/NP/NPC 入门是模块 4 的台阶,别跳。
数学手生?增长率比较(对数<幂<指数)、求和公式、矩阵运算的"三分钟复习"在上级目录 《数学基础工具箱》第 3、4、5 节;考前看第 8 节"急救包"。
1 · 通用框架与两类解空间树 ★★(算法填空必考)
void backtrack(int t) { // t: 当前扩展结点深度
if (t > n) output(x); // 到叶结点,得到一个可行解
else
for (int i = f(n,t); i <= g(n,t); i++) { // 候选取值
x[t] = h(i);
if (constraint(t) && bound(t)) // 剪枝:约束 && 限界
backtrack(t + 1);
}
}
迭代形式:while(t>0) 展开/回退(考试少考,了解)。| 子集树 | 排列树 | |
|---|---|---|
| 适用 | 从 n 元集选子集(每物选/不选) | 求 n 元的排列 |
| 叶结点 / 遍历时间 | \(2^n\) 个叶 / O(2ⁿ) | \(n!\) 个叶 / O(n!) |
| 典型问题 | 0-1 背包、装载、最大团 | TSP、批处理作业调度、n 皇后 |
void backtrack(int t) {
if (t > n) output(x);
else
for (int i = t; i <= n; i++) { // ①②
swap(x[t], x[i]); // ③
if (constraint(t) && bound(t)) backtrack(t + 1);
swap(x[t], x[i]); // 恢复现场
}
}2 · TSP / 0-1背包 / 装载(回溯版)
2.1 旅行商问题 TSP(排列树)
周游回路 = 固定起点 1,对 \(x[2..n]\) 全排列,末尾加回 \(x[1]\)。剪枝:当前部分和 \(cc\) ≥ 已知最优 bestf 即剪;再加上“图中有边”的可行性检查。n=4 时解空间 4!=24 个叶。
2.2 0-1 背包(子集树 + 双剪枝)★
限界函数(右子树,不装):计算经过当前结点的价值上界 = 当前价值 cv + 剩余物品按单位价值贪心装满剩余容量的价值(可拆!);上界 ≤ bestv 时剪去右子树。
另一例(Review S118,n=3, w={20,15,10}, v={20,30,25}, C=25):画解空间树并标搜索过程——最优 (0,1,1),w=25,v=55。
2.3 装载问题(两艘船)
n 个集装箱装两艘载重 \(c_1,c_2\) 的船。有解时最优策略:① 第一艘尽量装满(等价于特殊的 0-1 背包:子集和最接近 \(c_1\))→ 回溯子集树;② 剩余装第二艘。剪枝:约束 \(cw+w_i\le c_1\);限界 \(cw+r>wm\) 才展开右子树(r=剩余箱总重)。例:n=4, c₁=12, w={8,6,2,3} → 第一船装 {8,2}?不,可装 {8,3}=11 或 {6,2,3}=11 或 {8,2}=10… 目标是尽量靠近 12 而非一定装满 12:{6,2,3}+8=19>12;{8,3}=11、{6,2,3}=11 → 装满 12 需 {8,2}=10 +3=13 超;实际第一船最优 wm=11({8,3} 或 {6,2,3})。能否将剩余箱全装到第二船,仍须检查第二船载重 c₂:总重量为 19,故此例要求 c₂≥8。
3 · n 皇后 / 最大团 / 批处理 / m 着色
3.1 n 皇后(排列树 + 隐约束)★
bool place(int k) { for (int i = 1; i < k; i++)
if (x[i]==x[k] || abs(k-i)==abs(x[i]-x[k])) return false;
return true; }
4 皇后:解 (2,4,1,3) 与 (3,1,4,2)。解空间是完全 n 叉树(Review 标注为“排列树”视角)。3.2 最大团 / 最大独立集
团 = 两两有边的完全子图(极大、最大);独立集 = 两两无边。G 的最大团 = 补图 G′ 的最大独立集(等价,判断题)。回溯:子集树;约束=新顶点与团内每个顶点都有边;限界=cn+(n−i)≤bestn 剪右子树。O(n2ⁿ)。
3.3 批处理作业调度(排列树)
n 个作业各两道工序,先机器 1 后机器 2,求调度使完成时间和最小。例 n=3:6 种调度完成时间和 19,18,20,21,19,19 → 最优 (1,3,2)=18。排列树 + 剪枝:当前完成时间和 ≥ 最优值即剪。机器 2 有空闲与积压两种情况要分别累计。
3.4 图的 m 着色
给定无向图 G 与 m 色,相邻顶点异色。解空间:高 n 的完全 m 叉树。剪枝:与已着色邻居不同色(Bound 检查)。应用:考试排期(课程=顶点、学生重叠=边、颜色=时段);地图着色经对偶图转化为顶点着色(四色定理:平面图 4 色足够,1976 年机器证明)。问“存在性”(能否 m 着色)与“最少色数”两种问法。
3.5 回溯效率分析
效率取决于:解空间结构(重排原理:取值少的分量优先)、产生 x[k] 的时间、满足剪枝的结点数、约束/限界函数本身的计算代价——剪枝函数不宜太贵。
📄 期末禁止写代码:n 皇后问题(回溯法深度优先搜索) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:解空间表示为 \(n\) 元向量 \(x[1..n]\),其中 \(x[t]\in\{1,\dots,n\}\) 表示第 \(t\) 行皇后放置的列号。搜索树为高度 \(n\) 的完全 \(n\) 叉树(或全排列树)。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:约束剪枝函数 \(Place(t)\):第 \(t\) 行皇后与前面任意第 \(i\) 行(\(1\le i < t\))皇后不同列且不同正反斜线,即 \(x[i]\ne x[t]\) 且 \(|x[i]-x[t]| \ne |i-t|\)(编程时可用列集 `cols`、主对角集 `r-c`、副对角集 `r+c` 实现 \(O(1)\) 判重)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:从第 \(t=1\) 行开始递归搜索;第二步:若 \(t>n\) 说明找到一组完整合法布局,记录解并返回;第三步:否则在当前第 \(t\) 行依次尝试列号 \(col=1\dots n\),调用约束函数检验冲突;第四步:若不冲突则标记占用并递归进入第 \(t+1\) 行,回溯返回时务必撤销当前行的列与对角线占用标记。
- ④ 时间与空间复杂度严格推导:解空间树结点总数上界 \(O(n^n)\)(排列树视角为 \(O(n!)\)),每次约束检查 \(O(1)\)(哈希集)或 \(O(n)\),递归栈与状态标记空间复杂度 \(O(n)\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def n_queens(n):
"""每种方案为逐行皇后列号,最坏指数搜索。"""
if n < 0:
raise ValueError('n 不得为负')
ans, path = [], []
cols, d1, d2 = set(), set(), set()
def search(row):
if row == n:
ans.append(path[:])
return
for col in range(n):
a, b = row - col, row + col
if col in cols or a in d1 or b in d2:
continue
cols.add(col)
d1.add(a)
d2.add(b)
path.append(col)
search(row + 1)
path.pop()
d2.remove(b)
d1.remove(a)
cols.remove(col)
search(0)
return ans
4 · NP 完全性入门 ★
(本节是模块 4 的预告与浓缩;完整展开——含归约链、证明模板、网络流、近似算法——见模块 4。)
经典 NPC 问题清单(判断题常客):SAT / 3-SAT、0-1 背包(判定版)/ 子集和、TSP、Hamilton 回路、顶点覆盖、团、独立集、图的 m 着色(m≥3)。
5 · 分支限界:思想与回溯的三大区别 ★★
| 回溯法 | 分支限界法 | |
|---|---|---|
| 求解目标 | 找出满足约束的所有解 | 找出一个解或某种意义下的最优解 |
| 搜索方式 | 深度优先 DFS | 广度优先 BFS / 最小耗费(最大效益)优先 |
| 活结点机会 | 可多次成为扩展结点 | 只有一次机会成为扩展结点(一次性产生全部儿子) |
6 · 队列式 / 优先队列式
优先队列式:每个活结点带优先级(耗费最小/效益最大),用堆选取——跳向最有希望的结点,通常更快到达最优;叶结点成为扩展结点时即可停(其界已压过所有活结点)。FIFO 与优先队列的 PT 表数据结构不同(队列 vs 堆)——思考题原话。
7 · 六大经典问题(分支限界版)
| 问题 | 解空间 | 界 / 优先级设计 | 要点 |
|---|---|---|---|
| 0-1 背包 | 子集树 | 上界 ub=贪心(单位价值装满剩余容量);例:W=10,v₁/w₁=10 → 根 ub=100,贪心可行解给 down=40 | 左儿子=装入(须 ≤c);每结点存路径以重构解 |
| 单源最短路径 | 树(路径前缀) | 优先队列取当前 length 最小者扩展;dist[j] 作控制约束(新长度 < dist[j] 才入队) | 非负权;先到 t 的叶即最短(BFS 性质保证) |
| 装载 | 子集树 | 优先级 = 当前载重 cw+剩余 r;约束 cw+w≤c₁ | 叶结点成为扩展结点即可断言最优;等价于子集和最接近 c₁ |
| 布线问题 | 网格 BFS | 队列式:从起点向上下左右扩散,先到达目标 b 的即最短(否则它会更早入队);记录前驱回溯构造路径 | 回溯不适用:固定方向 DFS 会沿死路越走越远(思考题原话) |
| 最大团 | 子集树 | 优先级 un=cn+r(当前团顶点数+剩余可选);约束:与团内每点有边 | cn=当前团顶点数;叶结点 un=cn 时停 |
| TSP(售货员) | 排列树 | 最小堆;优先级 lcost(当前费用下界) | s=n−1 层叶结点若可行且更小则更新;叶成为扩展结点时其余活结点界均不更小,算法结束 |
另有:批处理作业调度(3 机器版:下界 sum3db=机器 1、2 无空闲且最后是机 3 时间最短作业时的理想值;例 4 作业下界 36);电路板排列(排列树,最小化连接块密度,优先队列式)。
📄 期末禁止写代码:0-1 背包问题(优先队列式分支限界法) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:首先将物品按单位重量价值 \(v_i/w_i\) 降序重排。解空间为高度 \(n\) 的子集树,活结点记录当前层号 \(i\)、已装重量 \(cw\)、已获价值 \(cv\) 及线性松弛上界 \(ub\),维护全局当前最优值 \(bestv\)。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:可行性剪枝(左孩子装入物品 \(i\)):仅当 \(cw + w_i \le C\) 时生成左孩子并尝试更新 \(bestv\);上界限界函数 \(Bound(i,cw,cv)\):对剩余容量 \(C-cw\) 贪心装入后续整件物品,放不下时用下一件物品的单位价值装满剩余空隙得分数背包上界 \(ub\);仅当 \(ub > bestv\) 时才将孩子结点压入最大优先队列。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:按 \(v_i/w_i\) 降序排列物品,计算根结点上界并压入以 \(ub\) 为优先级的大根堆;第二步:每次弹出堆顶 \(ub\) 最大的活结点作为扩展结点,若其 \(ub\le bestv\)(由于堆单调性)算法可直接终止;第三步:分别考察左分支(选第 \(i\) 件,需过容量约束与上界检验)和右分支(不选第 \(i\) 件,需过上界检验),将合格孩子入堆;第四步:堆空或提前剪枝终止时返回 \(bestv\)。
- ④ 时间与空间复杂度严格推导:最坏情况子集树有 \(O(2^n)\) 个结点,单次计算上界耗时 \(O(n)\),堆操作 \(O(n)\),最坏时间 \(O(n 2^n)\),活结点优先队列空间最坏 \(O(2^n)\),但平均剪枝效率远优于回溯。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def knapsack_branch_and_bound(weights, values, capacity):
"""优先队列式分支限界求解 0-1 背包最优值(按单位价值线性松弛上界剪枝)。"""
items = sorted(zip(weights, values), key=lambda x: x[1] / x[0], reverse=True)
n = len(items)
def bound(idx, cur_w, cur_v):
rem = capacity - cur_w
b = float(cur_v)
while idx < n and items[idx][0] <= rem:
rem -= items[idx][0]
b += items[idx][1]
idx += 1
if idx < n and rem > 0:
b += rem * (items[idx][1] / items[idx][0])
return b
best = 0
pq = [(-bound(0, 0, 0), 0, 0, 0)] # (-ub, idx, cur_w, cur_v)
while pq:
neg_ub, idx, cur_w, cur_v = heappop(pq)
if -neg_ub <= best or idx == n:
continue
w, v = items[idx]
if cur_w + w <= capacity:
nv = cur_v + v
if nv > best:
best = nv
ub_left = bound(idx + 1, cur_w + w, nv)
if ub_left > best:
heappush(pq, (-ub_left, idx + 1, cur_w + w, nv))
ub_right = bound(idx + 1, cur_w, cur_v)
if ub_right > best:
heappush(pq, (-ub_right, idx + 1, cur_w, cur_v))
return best
8 · 公式速查(一页纸素材)
| 回溯框架 | DFS + constraint(约束) + bound(限界)=剪枝函数;空间 O(h(n)) |
| 两类树 | 子集树 2ⁿ 叶 / O(2ⁿ)(背包、装载、最大团);排列树 n! 叶 / O(n!)(TSP、批处理、n 皇后) |
| 排列树要点 | swap(x[t],x[i]) → 递归 → swap 回 |
| 回溯 vs 分支限界 | 所有解 vs 一个/最优解;DFS vs BFS/优先级;多次扩展 vs 一次扩展;省空间 vs 费空间 |
| 分支限界两种 | 队列式 FIFO;优先队列式(最大堆=效益,最小堆=耗费) |
| 0-1 背包限界 | 约束 cw+w≤c;限界 = cv + 剩余容量按单位价值贪心装满的可拆价值 |
| 装载限界 | 右子树剪枝:cw+r > bestw 才展开 |
| n 皇后隐约束 | xᵢ≠xⱼ 且 |i−j|≠|xᵢ−xⱼ| |
| 最大团 | G 最大团 = 补图最大独立集;O(n2ⁿ) |
| 布线 | BFS 先到即最短;前驱数组回溯路径 |
9 · 自测(含算法填空真题)
if (t>n) _①_(output(x))else for (int i=0; _②_; i++)(i<=1){ _③_; if (constraint(t)&&bound(t)) backtrack(t+1); }(x[t]=i)。排列树框架:for (int i=__①__; ②; i++) { ③; if(…) backtrack(t+1); ④; }。(t;i<=n;swap(x[t],x[i]);swap 回)
- 一个活结点最多有一次机会成为扩展结点的是:A 回溯法 B 分支限界法。(B——Review 选择原题)
- 回溯法只能寻找所有解。(×;也可找到第一个可行解或最优解,取决于目标与剪枝方式。)
- 分支限界法的存储空间比回溯法小。(×,相反)
- 最大团问题与最大独立集问题等价。(√——经由补图)
- 布线问题用回溯法同样高效。(×,固定方向 DFS 会沿死路走远,应 BFS)
① 画解空间树(子集树 8 叶);② 写约束(cw+wᵢ≤30)与限界(设 r=剩余价值,r+cv≤bestv 剪右子树);③ 给出用分支限界求解的过程(贪心可行解 (1,0,0) 价值 45 作下界;上界=按单位价值装满)。参照 §7 表格模板作答。
🎯 考场题型信号词 → 破题套路速查表 期末冲刺必背 · 信号词秒配定理与防扣分清单
考场上读题 10 秒内锁定“题干信号词”,直接调用中间列的定理模板作答,并在交卷前对照右列逐条排查高频扣分点。
| 🔍 题干信号词 / 设问特征 | ⚡ 必用定理 / 核心递推式 / 标准破题套路 | 🚨 老师批改高频扣分点 |
|---|---|---|
| 简述回溯法与分支限界法的三大核心区别(高频简答/辨析题) | 三维度对比:① 搜索目标:回溯求全部可行解或最优解,分支限界专求最优解(或单个可行解);② 搜索方式:回溯是深度优先(DFS),分支限界是广度优先或最小耗费/最大效益优先(BFS/最佳优先);③ 结点扩展:回溯每次仅生成 1 个儿子纵深推进,分支限界一次性生成扩展结点的所有可行儿子 | ⚠ 避坑:漏答“扩展结点生成儿子方式不同”这一教材核心踩分点 |
| 判断问题解空间树类型(子集树 vs 排列树)并写出递归回溯框架 / 算法填空 | 从 \(n\) 个元素选子集(0-1 背包/装载/最大团)用子集树(\(0/1\) 二分分支,叶数 \(2^n\),遍历 \(O(2^n)\));求元素全排列(TSP/批处理作业调度)用排列树(`swap(x[t], x[i])` 交换再还原,叶数 \(n!\)) | ⚠ 避坑:排列树回溯算法填空中,递归调用 `Backtrack(t+1)` 返回后漏写第二个 `swap(x[t], x[i])` 恢复现场! |
| 设计回溯剪枝函数(约束函数 `Constraint` vs 限界函数 `Bound`) | 左分支(选入/装入)重点检查显式可行性约束(如容量 \(cw+w_i\le C\)、与已选团全相连);右分支(不选/放弃)重点检查上界限界函数(如当前价值 + 剩余最大可能价值 \(> bestv\)) | ⚠ 避坑:0-1 背包或最大团回溯时,进入左子树也冗余调用上界函数,或进入右子树忘记调用上界限界函数导致退化为暴力枚举 |
| 手工演算优先队列式分支限界法求解 0-1 背包 / 旅行商 TSP / 单源最短路 | 先按单位价值排序算线性松弛上界 \(ub\)(或 TSP 出边最小和下界 \(lb\));按大根堆(最大化问题)或小根堆(最小化问题)顺序列出每步弹出的扩展结点及生成的左右活结点状态表 | ⚠ 避坑:活结点已装重量超容仍入队;或者当堆顶活结点的上界 \(ub \le bestv\) 时未及时剪枝终止 |
| 装载问题 / 最大团问题 / 图的 \(m\)-着色问题建模与复杂度分析 | 最大团:子集树,左剪枝看顶点 \(t\) 是否与当前团内所有点均有边相连,右剪枝看 \(cn + (n-t) > bestn\);\(m\)-着色:完全 \(m\) 叉树,总结点数 \(\frac{m^{n+1}-1}{m-1}\),时间 \(O(nm^n)\) | ⚠ 避坑:把 \(m\)-着色问题的解空间树误当成子集树(应为度为 \(m\) 的完全 \(m\) 叉树) |