怎样判断状态信息够不够
从“规模更小的同类问题”开始。尝试写递推时,若无法判定能否把当前选择接上去,就检查状态是否漏掉边界信息。LIS 中,这个信息是上一段子序列的最后一个元素。
老师在课后答疑补充:子问题是“关于参数的一个问题”。只有一个参数名 i、k 或 C,尚未定义它究竟要求长度、次数、可行性还是最优值。
优先使用足够且必要的信息。多加参数会增加状态数;删掉未来判断需要的信息,会使递推失效。
动态规划 · 两节 lecture 分界 · 从状态设计到背包与 LCS
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L2、80 · 搜索:starting our next module which is on dynamic programming;it's the first thing that we'll do on Thursday
本次主题是动态规划(Dynamic Programming, DP):把大问题拆成能重复利用的小问题,先求小答案,再按明确规则求大答案。重点是定义状态、说明递推为什么成立,以及怎样从状态得到原题答案。
| 阅读顺序 | 这一部分解决什么 | 课件实际页码 |
|---|---|---|
| 汉诺塔 → DP 思想 | 相同子问题的数值答案为何只算一次 | p3–17 |
| 跳格 Hopscotch | 按最后一步分组,怎样数清所有方案 | p19–22 |
| LIS | 状态为何要固定结尾;怎样证明并恢复序列 | p24–32 |
| 活动选择:最大总时长 | 在结束时间排序后,把 LIS 的结构迁移过来 | p33–37 |
| 找零 Making Change | 为何最大面值优先会失败;本次在这里结束 | p38 |
本页使用 week5 文件夹中的 3-dp.pdf 与两份 transcript。第一份 TXT 的主要讲授在原文 L2、L80,第二份在 L2、L14、L20、L26;单行很长,以真实行号加可搜索短语定位。PDF 实际页码比页脚编号大 1。各模块来源标明 Lecture 1 或 Lecture 2,例子与补全证明另行标注。
先读第 02–04 节建立框架,再读 LIS 的状态与证明,最后把相同思路迁移到活动选择。每个演示旁都给出规则;可以随时单步、回退或重置。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L2 · 搜索:we don't have to report the sequence of moves, just the number of moves;the answer is the same
Instance: There are three pegs and n disks of radius 1, 2,..., n. Initially, all disks are on the first peg, in order of radius. In each move, you can move one disk from one peg to another, but you can never place a larger disk on top of a smaller disk. To win the game, stack all disks on the third peg (again in order of radius). Task: Find the minimum number of moves to win the game. Note: We do not have to report the actual moves.
实例:有三根柱子和半径依次为 1,2,…,n 的圆盘,开始时所有盘按大小叠在第一根柱上。每次移动一个盘,且大盘不能放在小盘上。目标是把所有盘按大小叠到第三根柱上。 任务:求完成目标所需的最少移动次数,无须输出具体移动过程。
输入是 n 个从小到大叠放的圆盘和三根柱子;输出是从第一根柱子搬到第三根柱子所需的最少移动次数。课件求的是一个数,因此可以复用相同规模的数值答案。
以 4 个圆盘为例:先把较小的 3 个搬到中间柱,把最大的圆盘移到目标柱,再把那 3 个搬到目标柱。前后两个“搬 3 个”的起点和终点不同,但柱子的名字不影响最少次数。
| 阶段 | 需要做什么 | 次数 |
|---|---|---|
| 清空最大圆盘上方 | 把 1 至 n−1 号盘从起点搬到辅助柱 | moves(n−1) |
| 移动最大圆盘 | 把 n 号盘搬到目标柱 | 1 |
| 完成剩余圆盘 | 把 1 至 n−1 号盘搬到目标柱 | moves(n−1) |
moves(1) = 1
moves(i) = 2 × moves(i−1) + 1 (i ≥ 2)
i 1 2 3 4 5
moves(i) 1 3 7 15 31若把两个 moves(n−1) 都重新递归计算,调用树会反复展开相同问题,计算时间为 Θ(2ⁿ)。保存较小规模的答案后,每个规模只做一次“乘 2 加 1”,按 i=1,2,…,n 求值,课件中的算术操作次数是 Θ(n)。
绿色:当前状态;蓝色:合法依赖;金色:最终路径。演示展示计算过程;一般正确性见正文证明。
设 M(n) 是任意合法方案的最少移动次数。第一次移动最大盘前,所有较小盘必须在同一根其他柱上,至少需要 M(n−1) 次。最大盘最后一次移到目标柱后,较小盘又必须从另一根柱搬到目标柱,至少还需 M(n−1) 次;最大盘至少移动一次。因此每个合法方案至少需要 2M(n−1)+1 次。
课件的三阶段方案恰好达到这个下界,所以 M(n)=2M(n−1)+1。这个下界也涵盖最大盘被移动多次的方案。
本例可以解出 moves(n)=2ⁿ−1,但后续 DP 未必有易用的闭式。老师用 2²⁰²⁶ 举例:先求 2¹⁰¹³ 再平方;奇数指数多取出一个 2。
偶数 k:2ᵏ = (2^(k/2))²
奇数 k:2ᵏ = 2 × (2^((k−1)/2))²每层只计算一次较小的幂,再复用它进行平方,指数逐次减半,因此乘法次数为 O(log n)。这里和课件的 Θ(n) 分析都沿用基本算术操作模型;结果有 n 量级的二进制位,精确大整数运算与输出成本另计。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L2、14、20 · 搜索:those are the six, points that you want to hit;the competency is always to define the process
DP 把原题变成一组有明确含义的子问题。状态(state)就是“带参数的问题”:先说清这一格问什么,再决定这一格存什么。Lookup table 是保存答案的数组;状态的含义决定哪些旧答案可以用。
| 必须说明的部分 | 要回答的具体问题 |
|---|---|
| Subproblems · 子问题 | 参数取值范围是什么?每格对应哪一个精确问题? |
| Recurrence · 递推 | 当前答案怎样由哪些较小状态求得?哪些转移合法? |
| Base cases · 基础情况 | 递推用不到或不能继续向下使用时,答案是什么? |
| Order of computation · 计算顺序 | 计算当前状态前,它依赖的状态是否已经求出? |
| Overall answer · 原题答案 | 返回某一个状态,还是把多个状态合并? |
| Time complexity · 时间复杂度 | 预处理 + 所有状态的计算 + 最后提取答案,各花多少时间? |
输入 → 定义子问题 → 求出基础情况
→ 按依赖顺序使用递推并保存答案
→ 提取原题答案| 两种关键性质 | 课件英文原文 | 中文解释 |
|---|---|---|
| Substructure property | answers to smaller subproblems (child instances) can be combined to answer a larger subproblem (parent instance). | 小问题的答案包含组合出大问题答案所需的信息。最优化题中,还要解释为何可以使用最优的子答案。 |
| Overlapping subproblems property | the same subproblem occurs several times in the recursion tree. | 递归会重复遇到相同子问题;保存一次结果后,可以直接查询。 |
教授称 DP 为 efficient brute force:在所定义的转移中考虑所有合法选择,同时复用已经算好的子答案。贪心只保留局部选择,因而需要说明为何舍弃其他选择仍然安全;DP 的证明重点是转移是否完整、合法,子答案能否支持后续组合。
分治的典型例子是归并排序:两半各自排序,再合并。DP 的典型效率来源是重叠子问题。仅仅写了递归,并不能保证相同问题只计算一次。
课程默认采用自底向上(bottom-up):先基础情况,再逐渐求较大状态。自顶向下(top-down)实现也可行,但必须缓存已求答案;记忆化(memoization)指的就是这种缓存。
本次课件按两种代价分类:一参数、常数递推(one parameter, constant recurrence)表示每个状态只查询固定个数的旧答案,如汉诺塔与跳格;一参数、线性递推(one parameter, linear recurrence)表示每个状态可能扫描线性数量的候选,如 LIS 与活动选择。这里“常数 / 线性”说的是求一个状态的工作量,不是原题答案大小(目录见 PDF p18、p23)。
从“规模更小的同类问题”开始。尝试写递推时,若无法判定能否把当前选择接上去,就检查状态是否漏掉边界信息。LIS 中,这个信息是上一段子序列的最后一个元素。
老师在课后答疑补充:子问题是“关于参数的一个问题”。只有一个参数名 i、k 或 C,尚未定义它究竟要求长度、次数、可行性还是最优值。
优先使用足够且必要的信息。多加参数会增加状态数;删掉未来判断需要的信息,会使递推失效。
| 证明对象 | 需要说明 |
|---|---|
| 基础情况 | 最小问题的直接答案正确。 |
| 递推:合法性 | 每个候选都满足题目限制。 |
| 递推:完整性 / 最优性 | 每个合法解属于考虑过的情况;最优解的前一部分可以由相应最优子答案代替。 |
| 计算顺序 | 右侧依赖已经求出,且依赖逐次缩小。 |
| 最终答案 | 提取方式覆盖原题所有允许的终点或情况。 |
计数题通常需要把方案分成互不重叠、也没有遗漏的类别;最优化题通常要证明候选合法,并且没有更好的解被遗漏。下面两个例题分别展示这两种论证。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L2 · 搜索:our last move has to either be a step;There is one way to do this
Instance: Rui is playing a version of hopscotch on a linear court. They start in square 0 and want to get to square n. From any square, Rui can step one square forward, or jump two squares forward. Task: Design a linear time algorithm to count the number of ways that Rui can reach square n.
实例:Rui 从直线场地上的 0 号格出发,要到达 n 号格;每次可向前走 1 格,或向前跳 2 格。 任务:设计线性时间算法,计算到达 n 号格的不同方式数量。
输入为终点格 n,每次只允许向前走 1 格或跳 2 格;输出从 0 到 n 的不同动作序列数量。同一组动作的顺序不同,可能形成不同方案,例如 1,2 与 2,1。
先看 n=3:三种方案是 1+1+1、1+2、2+1。按照最后一步分类,“以 1 结束”的两种方案来自到达 2 的方案,“以 2 结束”的一种方案来自到达 1 的方案。
ways(i) = 从 0 到 i 的动作序列数量 (0 ≤ i ≤ n)
ways(0) = 1
ways(1) = 1
ways(i) = ways(i−1) + ways(i−2) (i ≥ 2)| 最后一步 | 到达前的位置 | 如何得到一整个类别 |
|---|---|---|
| 走 1 格 | i−1 | 每个到达 i−1 的方案后面加一个 1 |
| 跳 2 格 | i−2 | 每个到达 i−2 的方案后面加一个 2 |
证明递推:每个到达 i 的方案,最后一步必然是 1 或 2,因而没有遗漏。两类的最后一步不同,因而不会重复。删去最后一步又能唯一恢复此前的方案,所以两类数量恰好分别为 ways(i−1) 和 ways(i−2),相加即可。
| i | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| ways(i) | 1 | 1 | 2 | 3 | 5 | 8 |
绿色:当前状态;蓝色:合法依赖;金色:最终路径。演示展示计算过程;一般正确性见正文证明。
| 六部分中的其余项 | 结论及理由 |
|---|---|
| 计算顺序 | i=0,1,2,…,n;求 i 前,i−1 与 i−2 已经求出。 |
| 原题答案 | ways(n),与题目要求直接一致。 |
| 时间 | O(n) 个状态,每个只查询并相加两个已知值,合计 O(n)。 |
| 空间 | 保存完整表需要 O(n);只求最终数量时,只保留最近两项可用 O(1) 个数值槽位(补充实现观察)。 |
课件 p21 把子问题范围印为 1≤i≤n,老师口头修正为 0≤i≤n,以包含 ways(0)。本页采用修正后的范围。
基础情况相当于先推倒的多米诺;递推保证已知前两项时,可以求出下一项。正确性证明据此逐项归纳。这里得到的数列是 Fibonacci 数列的一个偏移。
老师提醒:包含无理数幂的闭式会在浮点运算中引入误差;课程重点是使用整数递推规定求值过程。此处 O(n) 仍采用课件的基本算术操作模型。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L80 · 搜索:the longest increasing subsequence, ending at;the default value of that max is 0
Instance: a sequence of n real numbers A[1..n]. Task: determine the length of a longest subsequence (not necessarily contiguous) in which the values in the subsequence are strictly increasing.
实例:给定 n 个实数组成的序列 A[1..n]。 任务:求最长严格递增子序列的长度;选出的元素可以不连续,但须保持原先的出现顺序。
最长递增子序列(Longest Increasing Subsequence, LIS)保留原数组中的先后顺序,可以跳过元素,选出值严格递增的序列。原数组不能排序;排序会改变问题允许的先后顺序。
课件例子 A=[15,11,13,16,12,17,18,14] 中,可以选 [11,13,16,17,18],长度为 5。子序列(subsequence)可不连续;连续的一段通常叫子数组(subarray)。严格递增要求 <,相等元素不能直接相接。
自然的初稿状态是“前 i 项的 LIS 长度”。老师指出,它没有告诉我们最优序列最后选了谁,因此单凭这一数字难以判断是否能接入新元素。
len(i) = A[1..i] 中,必须以 A[i] 结尾的 LIS 长度
合法前驱 j 的条件:
1 ≤ j < i 保持原数组中的先后顺序
A[j] < A[i] 保持值严格递增
len(1) = 1
len(i) = 1 + max({len(j) : 1 ≤ j < i 且 A[j] < A[i]} ∪ {0})公式中的 1 对应当前元素 A[i]。对每个合法前驱 j,使用以 A[j] 结尾的最长序列,再接 A[i];从这些候选中选长度最大者。写入 {0} 使没有前驱时也有明确结果:只选 A[i],长度为 1。
为什么对每个 j 只需最长的一条?所有以 A[j] 结尾的合法序列,都能在相同条件下接上 A[i],而接上之后都只增加 1。较短者不会优于较长者,这正是可以复用最优子答案的理由。
| i | A[i] | 合法前驱下标 j | len(i) | 选取的 pred(i) |
|---|---|---|---|---|
| 1 | 15 | 无 | 1 | — |
| 2 | 11 | 无 | 1 | — |
| 3 | 13 | 2 | 2 | 2 |
| 4 | 16 | 1,2,3 | 3 | 3 |
| 5 | 12 | 2 | 2 | 2 |
| 6 | 17 | 1,2,3,4,5 | 4 | 4 |
| 7 | 18 | 1,2,3,4,5,6 | 5 | 6 |
| 8 | 14 | 2,3,5 | 3 | 3 |
绿色:当前状态;蓝色:合法依赖;金色:最终路径。演示展示计算过程;一般正确性见正文证明。
| 计算顺序 | 原题答案 | 时间与空间 |
|---|---|---|
| i=1,2,…,n | max{len(i) : 1≤i≤n} | 扫描前驱共 0+1+⋯+(n−1)=n(n−1)/2 次;再 O(n) 取最大值。时间 O(n²),保存长度表为 O(n) 空间。 |
前驱候选受到 A[j]<A[i] 的过滤。每次 i 改变,阈值 A[i] 都可能变大或变小;上一轮的最佳前驱不一定仍合法。单独保存全部前缀的最大 len 值,无法回答这种按终值筛选后的最大值。
老师也指出,按 A[i] 值从小到大处理可以尊重这些依赖,但还需保留 j<i 的条件。课堂采用更直接的原数组下标顺序。
课件 p32 提出 O(n log n) 的改进。老师只提到高级数据结构或换一种视角后使用二分搜索,没有在本次 transcript 中推导;这里保留为课堂提及,不展开新算法。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L80 · 搜索:valid and it's optimal;I don't need to store the entire sequence
本节证明上一节的状态确实得到最优长度,再说明怎样返回元素序列。以下论证对应课件 p28 的思路;严谨归纳形式放在补充展开中,包含空前驱的边界情况。
定义真实最优长度为 L*(i),递推计算结果为 L(i)。假设所有 j<i 均有 L(j)=L*(j)。每个构造出的候选都合法,故 L(i)≤L*(i)。
若最优序列只有 A[i],L*(i)=1≤L(i)。若长度至少为 2,设其倒数第二项为 A[m],则 m<i 且 A[m]<A[i];去掉末项后的长度不超过 L*(m)=L(m)。因此 L*(i)≤1+L(m)≤L(i)。
两边合并,L(i)=L*(i)。这一写法补上了课件形式化证明中未单列的“没有前驱”情况。
恢复实际序列只需为每个 i 多存一个前驱下标 pred(i):若最优候选来自 j,记录 pred(i)=j;若只选 A[i],记录无前驱。前驱应是使 len(j) 最大的合法下标,不能把“下标最大”当成“长度最大”。
课件结果:
i 1 2 3 4 5 6 7 8
A[i] 15 11 13 16 12 17 18 14
len(i) 1 1 2 3 2 4 5 3
pred(i) — — 2 3 2 4 6 3
从最大 len 所在的下标 7 开始:
下标:7 → 6 → 4 → 3 → 2 → 无前驱
数值:18 → 17 → 16 → 13 → 11
逆序:11 → 13 → 16 → 17 → 18顺着前驱每次下标严格减小,最多走 n 步,最后逆序得到答案,恢复时间为 O(n)。每个状态多存一个下标,空间仍为 O(n);若每格都复制整条序列,最坏会占 O(n²) 空间。
这是对课堂算法的辅助表达,使用课件的 1 起始下标;0 仅作为“无前驱”的哨兵。输入非空,空输入可直接返回长度 0 和空序列。
for i = 1 to n:
len[i] = 1
pred[i] = 0
for j = 1 to i−1:
if A[j] < A[i] and len[j] + 1 > len[i]:
len[i] = len[j] + 1
pred[i] = j
k = any index maximising len[k]
sequence = empty list
while k ≠ 0:
append A[k] to sequence
k = pred[k]
reverse sequence老师说明,写答案时应清楚交代各部分的理由,课堂的简洁归纳与替换论证已经传达核心理解。正式题目另有证明要求时,仍以该题要求为准。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L80 · 搜索:the maximum duration of all the activities;sorting by end time
Instance: A list of n activities with starting times sᵢ and finishing times fᵢ. Activities are compatible if they do not take place simultaneously. Task: Find the maximum total duration of a subset of compatible activities.
实例:给定 n 个活动,活动 i 的开始时间为 sᵢ,结束时间为 fᵢ;所选活动必须互相兼容。 任务:在兼容活动子集中,求最大的总持续时间。具体递推使用本课件的 fⱼ<sᵢ 条件。
这次的目标是房间被占用的总时长最大。每个活动的贡献是 fᵢ−sᵢ;它与前面贪心课里“最多选多少项”的目标不同,原有最早结束策略不能直接保证总时长最优。
先按结束时间非递减排序,使 f₁≤f₂≤⋯≤fₙ,耗时 O(n log n)。这里可以排序活动,因为原题要选活动集合,不要求保留输入列表顺序。后续下标都指排序后的活动;返回原活动身份时,排序记录中一起保留原编号。
t(i) = 前 i 个活动中,必须以活动 i 结束的兼容安排的最大总时长
t(0) = 0 空安排;0 是哨兵,不是实际活动
t(i) = (fᵢ−sᵢ) + max({t(j) : 1 ≤ j < i 且 fⱼ < sᵢ} ∪ {0})这与 LIS 的结构相同:固定最后一项 i,枚举倒数第二项 j,选择最好的合法前半段,再加上当前项的贡献。LIS 加 1;本题加时长 fᵢ−sᵢ。{0} 表示可以只选活动 i,也避免对虚构的 f₀ 做兼容性比较。
绿色:当前状态;蓝色:合法依赖;金色:最终路径。演示展示计算过程;一般正确性见正文证明。
| 六部分中的其余项 | 结论及理由 |
|---|---|
| 顺序 | 排序后的 i=1,2,…,n;所有前驱 j<i 的 t(j) 已计算。 |
| 最终答案 | max({t(i):1≤i≤n}∪{0})。最优安排可能在任一活动处结束,空列表返回 0。 |
| 时间 | 排序 O(n log n) + 枚举前驱 O(n²) + 提取最大值 O(n),总计 O(n²)。 |
| 恢复与空间 | 记录使候选最优的 pred(i),从最佳终点回溯,再逆序。长度表与前驱表共 O(n) 额外空间。 |
合法性:此前的安排本身兼容,最后一个活动 j 又满足 fⱼ<sᵢ,故接上 i 后仍兼容。此前所有活动都不晚于 j 结束,因此只需检查最后一个活动与 i 的衔接。
最优性:取任意以 i 结束的最优安排,若倒数第二个活动为 m,前半段必须是“以 m 结束”的最大总时长安排;否则替换为更好的同结尾安排,再接 i,就得到更大的总时长,矛盾。递推枚举所有合法 m,并包含只选 i 的情况,因此得到最优 t(i)。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L80 · 搜索:if our denominations are 134, and the total that we're trying to make is 6;our sub problem is not only like the parameters
Instance: You are given a sequence of n integer coin denominations d₁ < d₂ < ... < dₙ, and a target amount C. Assume d₁ = 1 (so that you can always make change for any integer amount) and that you have an unlimited supply of coins of each denomination. Task: compute the smallest number of coins needed to make change for C.
实例:给定 n 种递增的整数硬币面值和目标金额 C;最小面值为 1,每种硬币数量无限。 任务:求凑出恰好 C 所需的最少硬币数。
输入是整数硬币面值 d₁<d₂<⋯<dₙ 和目标金额 C,每种硬币数量不限,且 d₁=1。输出凑出恰好 C 所需的最少硬币数。1 分硬币保证每个非负整数金额都能凑出。
自然的贪心尝试是:尽量先用最大面值,再用次大面值补足。老师用课堂讨论说明,要否定这条策略,必须找到它确实比另一种方案用更多硬币的输入。
| 面值与目标 | 最大面值优先 | 更优或对比方案 | 能否否定贪心 |
|---|---|---|---|
| {1,5},C=3 | 1+1+1,3 枚 | 只有 1 分可用,也是 3 枚 | 不能 |
| {1,3,4},C=9 | 4+4+1,3 枚 | 3+3+3,也是 3 枚 | 不能;该对比仅打平 |
| {1,3,4},C=6 | 4+1+1,3 枚 | 3+3,2 枚 | 可以;2 枚严格更少 |
C=6 的最优值确实是 2:一枚硬币的面值都不是 6,而 3+3 可以用两枚达到 6。这个反例证明“最大面值优先”对任意面值系统不成立。
课后老师还讨论了状态设计的方向:“用 k 枚硬币能凑出的最大金额”只会得到 k 个最大面值,不能回答“恰好 C 的最少硬币数”;“是否恰好用 k 枚能凑到 C”若没有明确转移,也不足以形成算法。要同时写出参数和这个参数对应的问题。
老师提醒后续分析要关注目标金额 C。Transcript 最后几句有明显转写损坏,因此这里保留这个具体提醒;关于精确复杂度或难度分类,等后续实际讲授时再整理。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L8、14、20 · 搜索:every edge that we walk up the tree;keep track of where everything is in the heap
这两点来自课间对上一节贪心与图算法的答疑,放在单独的补充模块,主线仍是本次 DP。行政问题、直播故障和职业闲聊未编入算法笔记。
并查集按大小合并(Union by size):把小分量的根挂到大分量的根。老师解释,不使用路径压缩的这种合并树中,沿父指针向上,每越过一条边,所对应的子树规模至少翻倍;从 1 到至多 n,最多经历 log₂n 次翻倍,所以高度为 O(log n),一次查根也是 O(log n)。
增广堆(augmented heap):普通优先队列通常只直接暴露堆顶。如果额外维护“元素目前在堆数组中的位置”,就能定位内部元素并更新其优先级;交换时同步更新位置记录。老师用这点解释此前图算法中的更新操作。
关于 DP 基础情况的答疑:从递推能处理什么反推,找出太小而无法继续使用递推的情况,再直接给出这些答案。任意多手算几项并不是定义基础情况的主要依据。
开场与课间提到 23 prisoners 谜题,但 transcript 没有给出完整题面,也没有讲解解法,DP PDF 中也没有该题。本页只记录它被提及,待有完整材料后再整理。
输入时自动保存到当前浏览器。
Lecture 1 · COMP3121_COMP9101 Le-transcript.txt · L2、80 · 搜索:those are the six, points that you want to hit
以下为自写复习题,覆盖本次已讲内容。可以先记录判断,再单独展开答案;每题有清空按钮。
不能。递推计算的是一个数:moves(20)=2²⁰−1=1,048,575。约 20 轮是计算该数的算术工作量;实际搬盘或列出全部动作需要 1,048,575 步。
不移动就是从 0 到 0 的一个有效空动作序列,所以 ways(0)=1。设为 0 后,ways(2)=ways(1)+ways(0)=1,会漏掉直接跳 2 格的方案;正确答案为 2。
len(8)=3 只回答“必须以最后一项 14 结束”的问题。原题答案是 max len(i)=5,在下标 7 结束。若改用 ≤,求出的会是非递减子序列;严格递增题不能这样改。
固定终点就知道前半段最后何时结束,因而可用 fⱼ<sᵢ 判断能否接上 i。无合法前驱时,取空前半段贡献 0,仅选活动 i,得到 fᵢ−sᵢ。最终对所有终点取最大值。
这两种方案都是 3 枚,只能打平,不能据此否定。C=6 时,贪心得到 4+1+1 共 3 枚,最优为 3+3 共 2 枚;一枚不能凑出 6,所以两枚就是最优。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:where we left off we were talking about this making change problem;consider every total
输入是正整数面值 d₁<…<dₙ、目标金额 C≥0,每种硬币无限供应,课件假设 d₁=1。输出是恰好凑出 C 的最少硬币数。面值 1、3、4,C=6:贪心选 4+1+1 共 3 枚,最优是 3+3 共 2 枚。
不要先承诺某枚硬币最好。把金额从 0 算到 C;计算金额 i 时,枚举最后加入的一枚 dₖ,剩余金额 i−dₖ 的答案已经在表中。
opt(0) = 0
opt(i) = 1 + min { opt(i−dₖ) : 1≤k≤n 且 dₖ≤i }
按 i=1,2,…,C 计算;最终答案 opt(C)。| 金额 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 最少枚数 opt(i) | 0 | 1 | 2 | 1 | 1 | 2 | 2 |
算 i=6 时,选 1 的候选值为 1+opt(5)=3,选 3 为 1+opt(3)=2,选 4 为 1+opt(2)=3。这里每个候选的剩余部分都用已求出的最优解,再比较所有候选。
C+1 个状态,每个最多试 n 种面值:时间 O(nC),存数值表的空间 O(C)。若没有面值 1,需要把不可达金额设为 +∞;这属于去掉课件假设后的扩展。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:size of the input;pseudo polynomial
O(nC) 对“金额数值 C”是多项式,但计算机接收到的是 C 的二进制表示。正整数 C 只需要 ⌊log₂C⌋+1 位;C=0 也只需常数位。输入没有 C 个对象,只包含 n 个面值和一个目标金额。
| 改变目标金额 | 表示 C 的位数 | 固定 n 时,这个 DP 的工作量 |
|---|---|---|
| C≈1,000 | 约 10 位 | 与 1,000 成正比 |
| C≈1,000,000 | 约 20 位 | 与 1,000,000 成正比 |
位数翻倍,金额可以从 C 变成约 C²,运行时间也随金额大幅增长。令 b 为 C 的位数,C 可达约 2ᵇ,因此依赖 C 的逐金额扫描不保证关于二进制输入长度是多项式。
更精确的补充:输入位数与 log(C+1)+Σ log(dₖ+1) 同阶;舍弃大于 C 的无用面值后,可用 O(n log(C+1)) 概括有效数字编码。不要把“这个 DP 不是多项式时间”写成“已经证明所有算法都不可能是多项式时间”。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:just enough information to take one step back;arg min
opt(C) 只回答用了几枚。若要知道用了哪些面值,就在每个状态记录一个达到最优值的硬币编号 pred(i)。min 返回最小的函数值;arg min 返回使函数值最小的参数。并列最优时任选一个即可,它不表示“最小编号”。
pred(i) ∈ arg min { opt(i−dₖ) : 1≤k≤n 且 dₖ≤i }
x ← C
当 x>0:
k ← pred(x)
输出一枚面值 dₖ 的硬币
x ← x−dₖ面值按 [1,3,4] 编号,C=6 时 pred(6)=2,表示选面值 d₂=3。于是 6→3→0,输出 3、3。每次金额严格下降,能回到 0;回溯时间等于输出的硬币枚数。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:not enforcing that the knapsack is full to the brim;there is no predecessor
Instance: You have n items, the ith of which has weight wi and value vi. All weights are integers. You also have a knapsack of capacity C. You can take each item any (integer) number of times. Task: Choose a combination of available items which all fit in the knapsack and whose value is as large as possible.
给定 n 类物品,第 i 类有重量 wᵢ 和价值 vᵢ,重量为整数;背包容量为 C。每类可取任意非负整数次。选择总重量不超过容量且总价值尽可能大的组合。
输入是 n 类物品、正整数重量 wₖ、非负价值 vₖ 和整数容量 C≥0;每类可以拿任意非负整数次。输出是不超过容量的最大价值。正重量保证每次递推容量变小;若无限物品含零重量且正价值,目标会无界,不属于这里的递推范围。
这与找零相似,但有两处关键变化:每件增加的价值是 vₖ,目标由最少枚数变成最大价值;重量只需不超过容量,不要求恰好填满。
opt(i) = 容量不超过 i 的最大价值
若 i < minₖ wₖ:opt(i)=0,m(i)=0
否则 opt(i)=max { vₖ+opt(i−wₖ) : wₖ≤i }
m(i) 记录达到该最大值的一个物品编号。
按 i 从小到大计算;答案 opt(C)。自写例子:A 重 2、值 3,B 重 3、值 4,容量 5。容量 1 只能空着,价值 0;容量 4 可选 A+A,价值 6;容量 5 可选 A+B,价值 7。容量 1 的 0 是允许空包,不是“重量 1 可恰好装满”。
正确性沿用找零的替换论证:最优非空组合中移走一件 k,剩余必须是容量 i−wₖ 下的最优组合,否则替换后能提高总价值。无限供应允许把 k 加回;没有可装物品时用空包。时间 O(nC),表空间 O(C),同为伪多项式。回溯反复输出 m(i),并减去对应重量,遇到 m(i)=0 停止。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:optimal substructure property;avoiding item J;first K items
现在每件物品最多取一次。若继续只存“容量 i 的最优价值”,再选物品 k 时,就不知道 opt(i−wₖ) 是否已经用过 k。自写最小反例:只有一件重 2、值 3 的物品,容量 4;旧递推得到 6,实际最多只能得到 3。
把一条最优路径回溯出来再检查,也不能修好这个状态。教授强调两种缺口:可能有另一个并列最优组合恰好不含 k;也可能所有最优组合都含 k,而合适的剩余部分必须是某个价值较低但不含 k 的组合。只保存一个最优值和一条路径,都没有保存所需的受限最优解。
直接加“禁止某一件 j”仍不闭合:接着选 k 后,剩余要同时禁止 j、k;再往下就要记录更大的禁用集合。枚举任意禁用集合会有 2ⁿ 种,容量维度再乘 C。
这里失败的是“仅按容量定义的状态”不支持所需最优子结构;不是说 0/1 背包本身无法使用 DP。状态要足够精确,让递推右侧仍然是同一类子问题。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:increasing order of K;disregard that first case
Instance: You have n items, the ith of which has weight wi and value vi. All weights are integers. You also have a knapsack of capacity C. You can choose each item only at most once. Task: Choose a combination of available items which all fit in the knapsack and whose value is as large as possible.
给定 n 件物品及其整数重量和价值,背包容量为 C,每件最多选择一次。选择总重量不超过容量、总价值尽可能大的组合。
定义 opt(i,k):容量不超过 i、只允许前 k 件物品时的最大价值。k 表示允许考虑的前缀长度,不是已经选择的件数。
opt(i,0)=0;正重量下 opt(0,k)=0
若 wₖ>i:opt(i,k)=opt(i,k−1)
否则:opt(i,k)=max(opt(i,k−1), vₖ+opt(i−wₖ,k−1))
答案:opt(C,n)。补全正确性:任取只用前 k 件的最优解。若不含 k,它的价值不超过 opt(i,k−1);若含 k,移除后是前 k−1 件中容量 i−wₖ 的合法组合,价值不超过相应最优值。反过来,这两个候选各自能构成合法组合,且取 k 的分支不会重复使用 k。因此两者的最大值恰好最优;k=0 的空包作为归纳基础。
| 图中的量 | 含义 |
|---|---|
| 行 i | 容量,从上到下增加 |
| 列 k | 允许前 k 件,从左到右增加 |
| 当前格 (i,k) | 依赖左侧 (i,k−1),以及左上方 (i−wₖ,k−1) |
两个依赖都在前一列,因此可按 k=0,1,…,n 逐列计算;只要前一列已经完整求完,当前列内的 i 可用任意顺序。也可以按 i 从小到大逐行计算,但每行必须按 k 从小到大。按容量从大到小逐行处理整张二维表,会遇到尚未计算的较小容量依赖。
自写例子:A=(重量2,价值3)、B=(3,4)、C=(4,5),背包容量 5。前两件可选 A+B 得 7;考虑 C 后,跳过得到 7,取 C 得 5+opt(1,2)=5,所以仍选 A+B。
绿色为当前格,蓝色为依赖。背包演示沿用实验台:行是物品前缀,列是容量,与课件表格转置;单元含义一致。演示不代替证明。
有 (C+1)(n+1) 个状态,每格只比较至多两个候选:时间 O(nC),完整表空间 O(nC)。与无限背包时间形式相同,原因不同:无限背包是一维 O(C) 个状态、每格 O(n);这里是二维 O(nC) 个状态、每格 O(1)。
恢复组合可记录选/不选;从 (C,n) 开始,不选时转到 (i,k−1),选时输出 k 并转到 (i−wₖ,k−1),每次 k 都减少。课件 m(i,k) 记录组合中的最大物品编号;按选/不选记方向是同一回溯思想的补充写法。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L2 · 搜索:two parameter DP for the previous problem
课件把这留作休息时的变式。本节直接给出补充推导:仍定义“容量 i、仅用前 k 类”,但若取了第 k 类,剩余部分依然允许继续取第 k 类。
可重复:opt(i,k)=max(opt(i,k−1), vₖ+opt(i−wₖ,k))
不可重复:opt(i,k)=max(opt(i,k−1), vₖ+opt(i−wₖ,k−1))
两式均在 wₖ≤i 时允许取物品,否则只能跳过。关键差异是取物品分支的第二个参数。可重复时依赖同列更小容量,因此按 k 逐列计算时,列内 i 必须递增;不能照搬 0/1 背包的“列内任意顺序”。基础情况与答案保持 opt(i,0)=0、opt(C,n),时间 O(nC)。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L14 · 搜索:balance partition;weights are also the numbers
Instance: positive integers x1, ..., xn. Task: partition these integers into two subsets A and B with sums SA and SB respectively, so as to minimise |SA−SB|.
给定正整数 x₁,…,xₙ,把它们划分为 A、B 两组,组内和分别为 S_A、S_B,使 |S_A−S_B| 最小。
给一组正整数,每个数字按输入位置算一个独立元素,分到 A、B 两组,每个元素恰好归一组,目标是让两组和尽量接近。数字相等也仍是不同的两件物品。
T = x₁+…+xₙ
不妨令 S_A≥S_B,则 S_B≤T/2
|S_A−S_B| = (T−S_B)−S_B = T−2S_B
T 固定,因此最小化差值 ⇔ 在 S_B≤T/2 下最大化 S_B。| 原问题 | 对应背包实例 |
|---|---|
| 输入数字 xᵢ | 一件重量 wᵢ=xᵢ、价值 vᵢ=xᵢ 的物品 |
| 较小组的和不超过一半 | 容量 C=⌊T/2⌋ |
| 每个数字至多放进 B 一次 | 0/1 背包 |
| 背包选出的物品 | B;未选出的全部进入 A |
自写例子 [1,2,5]:T=8,容量 4。能达到的最大较小组和是 3(1+2),剩余组和为 5,最小差是 8−2×3=2。容量上限不代表一定能恰好取到 4。
正确性来自两方向的对应:任意分组可把较小组视为可行背包;任意可行背包也能与其补集组成完整分组,且背包和≤T/2。最大化背包价值正好最小化差值。构造实例 O(n),调用 0/1 背包 O(nT),总时间 O(nT),仍是伪多项式。教授强调可直接说明映射并调用已知算法,不必重写全部 DP。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L14、20 · 搜索:chat and cart;consider prefixes of the strings
Instance: two sequences A = ⟨a1, a2, ..., an⟩ and B = ⟨b1, b2, ..., bm⟩. Task: find the length of a longest common subsequence of A and B. Definition (subsequence): A sequence s is a subsequence of another sequence S if s can be obtained by deleting some of the symbols of S (while preserving the order of the remaining symbols).
输入两个序列 A、B,求它们的最长公共子序列长度。子序列定义:从 S 删除若干符号并保持剩余符号的相对顺序后得到的序列 s。
最长公共子序列(Longest Common Subsequence, LCS)要保留两个序列中共同出现且顺序一致的元素,可以跳过中间元素。例:chat 与 cart 的 LCS 是 cat,长度 3。不要混淆 substring(子串):子串要求连续,子序列不要求连续。
只找相同字母还不够,选中的匹配必须在两个序列中都保持顺序。教授先从首字母解释后缀,再把方向反过来,用课件中的前缀状态:len(i,j) 是 A 前 i 个字符与 B 前 j 个字符的 LCS 长度。这里 i、j 是长度,0 表示空前缀。
len(i,0)=0;len(0,j)=0
若 aᵢ=bⱼ:len(i,j)=1+len(i−1,j−1)
若 aᵢ≠bⱼ:len(i,j)=max(len(i−1,j),len(i,j−1))
最终答案 len(n,m)。末尾相同:把这个共同字符放在答案末尾,前面的部分去解两个更短的前缀。末尾不同:两个末位置不能同时被某个公共子序列使用,否则它们作为最后匹配的字符必须相同;因此至少舍弃一个,两个可能都试。两个都舍弃的情形已经包含在这些分支里,不需额外加一个候选。
按 i 递增、同一 i 内 j 递增,即字典序(lexicographic order)。当前格只依赖上方、左方或左上方:前一行已经做完,当前行左边也已经做完。若改成逐列,列内仍须从上到下,因为同列上方也可能被依赖。
绿色为当前格,蓝色为依赖。背包演示沿用实验台:行是物品前缀,列是容量,与课件表格转置;单元含义一致。演示不代替证明。
共有 (n+1)(m+1) 个状态,每格 O(1),时间 O(nm),完整表空间 O(nm)。这里 n、m 是实际序列长度,所以与背包中的容量数值 C 有不同的输入意义。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L20 · 搜索:the direction from which we came
求长度时顺便保存每格来自哪个方向。回溯从 (n,m) 开始:末尾匹配时沿左上走,并输出这个字符;不匹配时沿达到当前最优值的上方或左方走,不输出字符。任一坐标到 0 即停止,收集到的字符逆序后就是一个 LCS。
| chat / cart 的回溯 | 动作 | 逆序收集 |
|---|---|---|
| (4,4):t=t | 记 t,走到 (3,3) | t |
| (3,3):a≠r | 走左到 (3,2),保留最优长度 | t |
| (3,2):a=a | 记 a,走到 (2,1) | t,a |
| (2,1):h≠c | 走上到 (1,1) | t,a |
| (1,1):c=c | 记 c,走到 (0,0) | t,a,c → 反转 cat |
本表是笔记按课件示例手推的轨迹;课件 p57 的 CLRS 图使用另一对序列 ABCBDAB 与 BDCABA,同样按箭头回溯。并列时可任选一个保持最优值的方向,所以 LCS 不一定唯一。回溯至多减少 n+m 次坐标,耗时 O(n+m)。
输入时自动保存到当前浏览器。
Lecture 2 · COMP3121_COMP9101 Le-transcript_2.txt · L20、26 · 搜索:non-uniqueness;all four of those dependencies
Instance: three sequences A = ⟨a1, a2, ..., an⟩, B = ⟨b1, b2, ..., bm⟩ and C = ⟨c1, c2, ..., cℓ⟩. Task: find the length of a longest common subsequence of A, B and C.
输入三个序列 A、B、C,长度分别为 n、m、ℓ,求同时属于三者的最长公共子序列长度。
看似自然的做法是先算 A、B 的一个 LCS,再拿这个结果与 C 求 LCS。问题是第一步丢掉了其他同样好的候选,而第三个序列可能更适合另一个候选。
| 课件反例 | 结果 |
|---|---|
| A=abcdegg,B=acbeefg | abeg 和 aceg 都是长度 4 的 LCS |
| C=accedgf;若第一步选 abeg | LCS(abeg,C)=aeg,只有长度 3 |
| 同时考虑 A、B、C | aceg 在三者中都是子序列,真正最优长度为 4 |
正确状态是 len(i,j,k):A 的前 i 个、B 的前 j 个、C 的前 k 个字符的 LCS 长度。三条序列原长分别为 n、m、ℓ;这里 k 是第三条前缀长度,与背包的物品前缀参数是不同用途。
任一坐标为 0:len(i,j,k)=0
若 aᵢ=bⱼ=cₖ:
len(i,j,k)=1+len(i−1,j−1,k−1)
否则:
len(i,j,k)=max(len(i−1,j,k),
len(i,j−1,k),
len(i,j,k−1))
答案 len(n,m,ℓ);时间 O(nmℓ),完整表空间 O(nmℓ)。补全正确性:三个末尾相同时,可以把共同字符接在三个较短前缀的最优解后;任意解去掉末尾匹配后也受较短前缀最优值限制。末尾不全相同时,公共子序列不可能使用所有三个末位置,必至少舍弃一个,因此三个分支覆盖所有解;每个分支也能嵌入原三前缀。结合任一前缀为空的基础情况,可按计算顺序归纳。
按 i、再 j、再 k 递增的字典序计算。减 i 的状态在更早的外层;i 不变而减 j 的状态在更早的中层;只减 k 的状态在当前内层之前。三个末尾全匹配时的斜对角依赖也更早。并不是三维表的所有相邻格都要读,只读公式中的这四种可能坐标。