分治
把问题分为小实例,求解后合并。归并排序平衡划分满足 T(n)=2T(n/2)+Θ(n),每层线性、层数对数。二分仅解一半,为 T(n)=T(n/2)+Θ(1)。子问题数量与合并成本都重要,仅称递归不能确定增长。
归并与二分为何成本不同?
完整解答
归并解两半并合并,二分只保留一半。
贪心选择与交换论证
贪心立即选择局部有利项。最多不重叠区间可选最早结束,再处理兼容区间。最优方案首区间可换成最早结束者,不推迟其他开始,数量不变,剩余仍同类问题。最早开始缺少此论证,早长区间可阻挡许多短区间。最大总价值是不同问题,通常不能用此规则。
最早结束能解带价值区间调度吗?
完整解答
一般不能,最大价值不同于最大数量。
回溯与穷举
回溯扩展部分解,拒绝不可能分支,并撤销选择再试替代。n 项子集共 2^n 种,剪枝可能减少实际探索但最坏仍指数。小实例穷举可作为快速算法对照。剪枝需论证,不能因局部不够好就排除最优可能。
为何穷举验证输入需小?
完整解答
子集数量指数增长。
动态规划状态
动态规划复用重叠子问题。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。
为何硬币值必须为正?
完整解答
转移需到更小状态,零或负值破坏依赖顺序。
恢复与独立检查
只有最优值不一定告诉用户如何操作。每次改善状态保存选择的前驱,再逆向恢复。独立检查可行性:硬币总值等于目标,区间不重叠。小案例与穷举对照最优性,可区分实现错误与递推错误。零金额的空解有效,不可达正金额则无解。
[] 与 None 可互换吗?
完整解答
不能,[] 是零金额有效解,None 表示无解。
常见误解
- 贪心正确性针对具体目标与假设。
- 动态规划表需状态含义与有效依赖顺序。
实验准备
下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m08_greedy.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。
实验 1 — 区间调度
最大化兼容半开区间数量,端点可相接,穷举检查小实例。
"""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
- 改用最早开始,构造失败。
- 加零时长任务,解释拒绝。
- 为任务加价值,说明对照应如何改变。
完整解答
最早开始的 [0,10] 阻挡三个短区间。实验要求开始小于结束,零时长无效。带价值需比较总价值而非数量,并采用带权算法。
实验 2 — 最少硬币
检查六元的各状态并恢复硬币,无穷标记不可达。
"""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 -> []
- 测试 [2,5] 的一与七。
- 比较贪心与动态规划。
- 加入只返回数量的版本,比较存储职责。
完整解答
一不可达,七用二加五。[1,3,4] 的六需两枚,而非贪心三枚。仅计数仍需 best 状态,但可省恢复前驱。
练习与完整解答
先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。
解两半再合并是什么策略?
完整解答
分治。
比较六元的贪心与最优。
完整解答
贪心三枚,最优两枚。
给 [1,3,4] 的 best[0…4]。
完整解答
[0,1,2,1,1],依赖更小已就绪状态。
[1,3) 与 [3,5) 可同时选吗?
完整解答
可以,半开区间在共享端点不重叠。
二十任务多少子集?
完整解答
1,048,576,说明穷举需小规模。
说明最早结束如何替换最优首区间。
完整解答
结束不更晚,后续区间仍兼容,数量不变,剩余仍同类调度问题。
硬币递推为何覆盖每个最优解?
完整解答
非零解有最后硬币 c,移除剩 v−c。剩余若非最优,可替换改善整体,所以对所有 c 取最小。
为何 O(A) 不对 log₂ A 多项式?
完整解答
A 需约 log₂ A 位,A 对位数指数增长,需说明规模指值还是编码长度。
自测
选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。
贪心选择需要什么?
最多区间选择规则?
动态规划复用什么?
硬币计数 best[0] 是多少?
回溯最坏子集数?
恢复保存什么?
答案表
- A — 局部吸引力不足。
- B — 交换保留后续可行性。
- C — 记忆或表格避免重复工作。
- A — 零无需硬币。
- B — 每项可选或不选。
- C — 仅数值未必确定决策。
引导阅读
- MIT 算法资料 — 阅读动态规划,在例子中指出状态、基础与转移。
复习与下一步
对新优化任务先定义目标、可行解与状态,再选策略。模块 09 将研究执行这些过程的机器。
关键术语
| 术语 | 含义 |
|---|---|
| 最优子结构 | 最优解可由合适的最优子问题组合。 |
| 回溯 | 探索选择,拒绝不可行分支并撤销。 |
| 交换论证 | 替换最优选择而不恶化目标。 |