第13次内容
大约 6 分钟教学文档算法设计与分析
📚 课前回顾:贪心算法经典问题(摆动序列 & 过河问题)
上次课我们深入学习了贪心算法在两类典型问题中的巧妙应用:摆动序列(Wiggle Subsequence)和过河问题(Bridge Crossing)。这两个问题虽然背景完全不同,但都体现了贪心思想的核心:在每一步做出局部最优选择,从而构造全局最优解。
🔁 一、摆动序列(Wiggle Subsequence)
✅ 问题目标
在不改变元素相对顺序的前提下,找出数组中最长的摆动子序列长度。
- 摆动序列定义:相邻元素之间的差值严格正负交替(不能为0)。
💡 贪心核心思想
- 只关注“方向变化点”(即“峰”与“谷”),跳过单调段中的中间值。
- 通过一次遍历,记录上一个有效差值(
prev_diff),每当遇到方向反转(curr_diff * prev_diff < 0)或首个非零差值,就计为一个有效摆动点。
📈 示例
对于 nums = [1, 17, 5, 10, 13, 15, 10, 5, 16, 8],
有效摆动点为:1 → 17 → 5 → 15 → 5 → 16 → 8,长度为 7。
⏱️ 复杂度
- 时间:
O(n),空间:O(1)
🧠 启发
- 问题可转化为寻找转折点数量 + 1;
- 贪心优于动态规划:简洁高效,体现“状态机”思想(用
prev_diff隐式记录状态)。
🌉 二、过河问题(Bridge Crossing)
✅ 问题目标
N 个人带一个手电筒过桥,桥最多容两人,同行取较慢者时间,手电必须由人带回。求最小总耗时。
💡 贪心核心思想
- 每次处理最慢的两个人,因为他们的过桥代价最大,应尽早安排。
- 对最慢两人有两种策略:
- 快的人来回送:
2*a[0] + a[n-1] + a[n-2] - 快+次快配合送:
a[0] + 2*a[1] + a[n-1]
- 快的人来回送:
- 每次选择更优策略,逐步缩小问题规模。
📈 示例
[1, 2, 5, 10] → 最优方案总耗时为 17:
- 1&2过(2)→ 1回(1)→ 5&10过(10)→ 2回(2)→ 1&2过(2)
⏱️ 复杂度
- 时间:
O(n log n)(主要为排序),空间:O(1)
🧠 启发
- 贪心 ≠ 直觉:最快的人不总是最优“搬运工”;
- 多策略比较 + 逆向思维(从最慢入手)是关键;
- 边界处理(n=1,2,3)不可忽略。
🔑 总结:贪心算法的精髓
| 问题 | 贪心选择 | 状态表示 | 关键技巧 |
|---|---|---|---|
| 摆动序列 | 只保留方向转折点 | prev_diff | 符号判断 + 跳过平台 |
| 过河问题 | 每次最优送走最慢两人 | 排序后索引 | 两种策略比较 + 分治思想 |
贪心成功的关键:问题具有最优子结构,且局部最优能导向全局最优。同时,正确设计状态与策略是避免错误的核心。
本次内容
📌 回溯法的定义:
回溯法是一种通过深度优先搜索(DFS) 遍历解空间树(通常为隐式树)的算法技术。它在搜索过程中逐步构建候选解,并在发现当前部分解无法扩展为合法解时,立即放弃(剪枝) 并回退(回溯) 到上一步,尝试其他选择。
换句话说,回溯法是一种“试错 + 撤销”的策略:
- 尝试某种选择 →
- 若后续无法达成目标 →
- 撤销该选择 →
- 尝试下一个可能。
🔑 回溯法的核心要素:
- 解的表示:将问题的解表示为一个向量(如
[x₁, x₂, ..., xₙ]),每个分量在特定阶段从候选集中选择。 - 约束条件:用于判断当前部分解是否可能扩展为完整合法解。
- 目标函数(可选):在优化问题中用于评估解的优劣(回溯通常用于求所有解或一个可行解,而非最优解;若求最优,常需与分支限界结合)。
- 递归与状态恢复:通过递归探索选择,回溯时需恢复状态(如撤销对数组、集合、棋盘的修改)。
⏱️ 时间复杂度特点:
- 最坏情况下需遍历整个解空间,复杂度通常为指数级(如
O(kⁿ),k 为每步平均选择数)。 - 但通过剪枝(Pruning)(即提前终止无效分支),在实践中可显著提升效率。
✅ 与贪心、动态规划的区别:
| 方法 | 决策方式 | 是否可撤销 | 适用场景 |
|---|---|---|---|
| 贪心 | 局部最优,不可撤销 | ❌ | 具有贪心选择性质的问题 |
| 动态规划 | 全局最优,记忆化 | ❌ | 有重叠子问题 + 最优子结构 |
| 回溯法 | 尝试所有可能路径 | ✅ | 解空间有限、需穷举+剪枝 |
📝 小结:
回溯法 = 递归 + 选择 + 约束检查 + 回溯(状态撤销)
它是“聪明的穷举”——通过提前剪枝避免无效搜索,是解决离散组合问题的有力工具。
真题演练:
📌 回溯法精要总结
回溯法 = 递归 + 选择 + 撤销
用于系统性地枚举所有满足约束的解,适用于组合、排列、搜索类问题。
♛ N皇后问题
- 目标:在 N×N 棋盘放 N 个皇后,互不攻击(不同行、列、对角线)。
- 策略:
- 逐行放置(天然避免行冲突);
- 对每列检查:列、主对角线、副对角线是否已有皇后;
- 递归尝试 → 成功则记录解 → 回溯(恢复
'.')。
- 关键:
checkPosition实现冲突检测,chessBoard[row][col] = 'Q'后递归,返回后重置。
📱 电话号码的字母组合
- 目标:给数字串(如 "23"),输出所有对应字母组合(如 ["ad","ae",...])。
- 策略:
- 预置数字→字母映射;
- 递归逐位选择字母,构建路径;
- 到达末尾则加入结果;
- 回溯时
pop()恢复路径。
- 特点:无冲突约束,纯组合生成,结构清晰。
🔁 回溯通用模板(精简版)
def backtrack(path, index):
if 满足终止条件:
result.append(path[:])
return
for choice in 选择列表:
path.append(choice) # 做选择
backtrack(path, index+1) # 递归
path.pop() # 撤销选择
✅ 核心要点
- 适用场景:需要枚举所有可行解(如排列、组合、布局);
- 效率关键:尽早剪枝(如 N 皇后的冲突检查);
- 空间开销:递归栈 + 路径存储,一般为 O(解深度);
- 与贪心/DP区别:回溯穷举可行解,不追求“最优”而是“全部”或“存在”。
回溯法是解决离散组合搜索问题的基石,掌握其模板可应对大量经典题目。
