Ran Wei/计算机科学系列/08
English
计算机科学基础 — Ran Wei

模块 08: 算法设计

在分治、贪心、回溯与动态规划间选择,论证最优性而非相信直觉。

约 5 小时4 个时段2 个实验8 道练习6 道自测题

完成后你能够

  • 识别分治递推。
  • 构造贪心交换论证。
  • 探索并剪枝搜索树。
  • 定义动态规划状态与转移。
  • 恢复并检查解。

开始之前

建议先修模块: 05, 07.

了解递归与渐近界。实验解决小型调度与优化问题。

目录

学习计划

5 小时

四个 75 分钟时段,包含练习。拓展任务或不熟悉的先修知识可能需要更多时间。进度本地保存,两种语言共享。

时段 275 分钟
构建与探究
时段 375 分钟
应用与拓展
时段 475 分钟
推理与复习
1

分治

把问题分为小实例,求解后合并。归并排序平衡划分满足 T(n)=2T(n/2)+Θ(n),每层线性、层数对数。二分仅解一半,为 T(n)=T(n/2)+Θ(1)。子问题数量与合并成本都重要,仅称递归不能确定增长。

检查理解

归并与二分为何成本不同?

完整解答

归并解两半并合并,二分只保留一半。

2

贪心选择与交换论证

贪心立即选择局部有利项。最多不重叠区间可选最早结束,再处理兼容区间。最优方案首区间可换成最早结束者,不推迟其他开始,数量不变,剩余仍同类问题。最早开始缺少此论证,早长区间可阻挡许多短区间。最大总价值是不同问题,通常不能用此规则。

检查理解

最早结束能解带价值区间调度吗?

完整解答

一般不能,最大价值不同于最大数量。

3

回溯与穷举

回溯扩展部分解,拒绝不可能分支,并撤销选择再试替代。n 项子集共 2^n 种,剪枝可能减少实际探索但最坏仍指数。小实例穷举可作为快速算法对照。剪枝需论证,不能因局部不够好就排除最优可能。

检查理解

为何穷举验证输入需小?

完整解答

子集数量指数增长。

4

动态规划状态

动态规划复用重叠子问题。best[v] 表示金额 v 最少硬币数,基础零,不可达为无穷。对正硬币 c≤v,取 best[v−c]+1 最小值。按 v 递增让依赖就绪。k 种硬币、目标 A 的时间 O(kA)、空间 O(A),对数值 A 伪多项式,并非对 A 位长多项式。六元用 [1,3,4] 时贪心给 4+1+1,输给 3+3。

检查理解

为何硬币值必须为正?

完整解答

转移需到更小状态,零或负值破坏依赖顺序。

5

恢复与独立检查

只有最优值不一定告诉用户如何操作。每次改善状态保存选择的前驱,再逆向恢复。独立检查可行性:硬币总值等于目标,区间不重叠。小案例与穷举对照最优性,可区分实现错误与递推错误。零金额的空解有效,不可达正金额则无解。

检查理解

[] 与 None 可互换吗?

完整解答

不能,[] 是零金额有效解,None 表示无解。

6

常见误解

  • 贪心正确性针对具体目标与假设。
  • 动态规划表需状态含义与有效依赖顺序。
7

实验准备

下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m08_greedy.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。

8

实验 1 — 区间调度

最大化兼容半开区间数量,端点可相接,穷举检查小实例。

下载 m08_greedy.py

"""Earliest-finish interval scheduling, checked against exhaustive search."""
from itertools import combinations
def compatible(intervals):
    ordered = sorted(intervals)
    return all(a[1] <= b[0] for a, b in zip(ordered, ordered[1:]))
def schedule(intervals):
    result, end = [], float("-inf")
    for start, finish in sorted(intervals, key=lambda item: item[1]):
        if start >= finish:
            raise ValueError("nonempty intervals required")
        if start >= end:
            result.append((start, finish)); end = finish
    return result

jobs = [(0, 4), (1, 2), (2, 3), (3, 5), (4, 6)]
chosen = schedule(jobs)
optimal = max(len(subset) for size in range(len(jobs)+1) for subset in combinations(jobs, size) if compatible(subset))
print("chosen:", chosen, "optimal count:", optimal)
assert len(chosen) == optimal == 3
assert schedule([]) == []
实际运行输出
chosen: [(1, 2), (2, 3), (3, 5)] optimal count: 3
  1. 改用最早开始,构造失败。
  2. 加零时长任务,解释拒绝。
  3. 为任务加价值,说明对照应如何改变。
完整解答

最早开始的 [0,10] 阻挡三个短区间。实验要求开始小于结束,零时长无效。带价值需比较总价值而非数量,并采用带权算法。

9

实验 2 — 最少硬币

检查六元的各状态并恢复硬币,无穷标记不可达。

下载 m08_dynamic.py

"""Minimum coin count with reconstruction; impossible values return None."""
def min_coins(coins, amount):
    if amount < 0 or any(c <= 0 for c in coins):
        raise ValueError("nonnegative amount and positive coins required")
    best, previous = [0] + [float("inf")] * amount, [None] * (amount + 1)
    for value in range(1, amount + 1):
        for coin in coins:
            if coin <= value and best[value - coin] + 1 < best[value]:
                best[value] = best[value - coin] + 1; previous[value] = coin
    if best[amount] == float("inf"):
        return None
    selected = []
    while amount:
        coin = previous[amount]
        selected.append(coin); amount -= coin
    return selected

for coins, amount in [([1, 3, 4], 6), ([2, 4], 3), ([1, 3, 4], 0)]:
    result = min_coins(coins, amount)
    print(coins, amount, "->", result)
    if result is not None: assert sum(result) == amount
assert min_coins([1, 3, 4], 6) == [3, 3]
assert min_coins([2, 4], 3) is None
实际运行输出
[1, 3, 4] 6 -> [3, 3]
[2, 4] 3 -> None
[1, 3, 4] 0 -> []
  1. 测试 [2,5] 的一与七。
  2. 比较贪心与动态规划。
  3. 加入只返回数量的版本,比较存储职责。
完整解答

一不可达,七用二加五。[1,3,4] 的六需两枚,而非贪心三枚。仅计数仍需 best 状态,但可省恢复前驱。

10

练习与完整解答

先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。

练习 1 — 策略★

解两半再合并是什么策略?

完整解答

分治。

练习 2 — 硬币反例★

比较六元的贪心与最优。

完整解答

贪心三枚,最优两枚。

练习 3 — 状态表★★

给 [1,3,4] 的 best[0…4]。

完整解答

[0,1,2,1,1],依赖更小已就绪状态。

练习 4 — 兼容性★★

[1,3) 与 [3,5) 可同时选吗?

完整解答

可以,半开区间在共享端点不重叠。

练习 5 — 子集增长★★

二十任务多少子集?

完整解答

1,048,576,说明穷举需小规模。

练习 6 — 交换论证★★★

说明最早结束如何替换最优首区间。

完整解答

结束不更晚,后续区间仍兼容,数量不变,剩余仍同类调度问题。

练习 7 — 动态规划证明★★★

硬币递推为何覆盖每个最优解?

完整解答

非零解有最后硬币 c,移除剩 v−c。剩余若非最优,可替换改善整体,所以对所有 c 取最小。

练习 8 — 数值与编码规模★★

为何 O(A) 不对 log₂ A 多项式?

完整解答

A 需约 log₂ A 位,A 对位数指数增长,需说明规模指值还是编码长度。

11

自测

选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。

1

贪心选择需要什么?

2

最多区间选择规则?

3

动态规划复用什么?

4

硬币计数 best[0] 是多少?

5

回溯最坏子集数?

6

恢复保存什么?

答案表
  1. A — 局部吸引力不足。
  2. B — 交换保留后续可行性。
  3. C — 记忆或表格避免重复工作。
  4. A — 零无需硬币。
  5. B — 每项可选或不选。
  6. C — 仅数值未必确定决策。
12

引导阅读

  • MIT 算法资料 — 阅读动态规划,在例子中指出状态、基础与转移。
13

复习与下一步

对新优化任务先定义目标、可行解与状态,再选策略。模块 09 将研究执行这些过程的机器。

14

关键术语

术语含义
最优子结构最优解可由合适的最优子问题组合。
回溯探索选择,拒绝不可行分支并撤销。
交换论证替换最优选择而不恶化目标。