跳至主要內容

第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 个人带一个手电筒过桥,桥最多容两人,同行取较慢者时间,手电必须由人带回。求最小总耗时

💡 贪心核心思想

  • 每次处理最慢的两个人,因为他们的过桥代价最大,应尽早安排。
  • 对最慢两人有两种策略:
    1. 快的人来回送2*a[0] + a[n-1] + a[n-2]
    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) 遍历解空间树(通常为隐式树)的算法技术。它在搜索过程中逐步构建候选解,并在发现当前部分解无法扩展为合法解时,立即放弃(剪枝)回退(回溯) 到上一步,尝试其他选择。

换句话说,回溯法是一种“试错 + 撤销”的策略:

  • 尝试某种选择 →
  • 若后续无法达成目标 →
  • 撤销该选择 →
  • 尝试下一个可能。

🔑 回溯法的核心要素:

  1. 解的表示:将问题的解表示为一个向量(如 [x₁, x₂, ..., xₙ]),每个分量在特定阶段从候选集中选择。
  2. 约束条件:用于判断当前部分解是否可能扩展为完整合法解。
  3. 目标函数(可选):在优化问题中用于评估解的优劣(回溯通常用于求所有解或一个可行解,而非最优解;若求最优,常需与分支限界结合)。
  4. 递归与状态恢复:通过递归探索选择,回溯时需恢复状态(如撤销对数组、集合、棋盘的修改)。

⏱️ 时间复杂度特点:

  • 最坏情况下需遍历整个解空间,复杂度通常为指数级(如 O(kⁿ),k 为每步平均选择数)。
  • 但通过剪枝(Pruning)(即提前终止无效分支),在实践中可显著提升效率。

✅ 与贪心、动态规划的区别:

方法决策方式是否可撤销适用场景
贪心局部最优,不可撤销具有贪心选择性质的问题
动态规划全局最优,记忆化有重叠子问题 + 最优子结构
回溯法尝试所有可能路径解空间有限、需穷举+剪枝

📝 小结:

回溯法 = 递归 + 选择 + 约束检查 + 回溯(状态撤销)
它是“聪明的穷举”——通过提前剪枝避免无效搜索,是解决离散组合问题的有力工具。

真题演练:

  1. N皇后问题
  2. 电话号码的字母组合

📌 回溯法精要总结

回溯法 = 递归 + 选择 + 撤销
用于系统性地枚举所有满足约束的解,适用于组合、排列、搜索类问题


♛ 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区别:回溯穷举可行解,不追求“最优”而是“全部”或“存在”。

回溯法是解决离散组合搜索问题的基石,掌握其模板可应对大量经典题目。

上次编辑于:
贡献者: zilizhou