0 · 数学归纳法、DAG 拓扑序与交换论证(Exchange Argument)
- 动态规划(DP)本质就是“带记忆表的强数学归纳法”:写不出状态转移方程时,先问自己“最后一步做了什么选择?”把问题规模缩小一步,假设所有更小规模的子问题都已经算好存表。填表循环次序的唯一铁律:在计算大状态之前,它右边依赖的所有小状态必须已经算完(例如区间 DP `m[i][j]` 依赖更短的区间 `m[i][k]` 和 `m[k+1][j]`,所以最外层循环必须是区间长度 `L = j - i + 1` 从小到大!)。
- 贪心算法正确性证明的标准模板(交换论证法):假设存在最优解 \(O\) 与贪心解 \(G\) 在第一步(或第 \(k\) 步)不同。将 \(O\) 中的那个元素替换(或交换)成贪心选出的元素得到新解 \(O'\),利用贪心选择条件(如活动结束时间最早 \(f_{g_1} \le f_{o_1}\))证明 \(O'\) 依然合法且目标值不劣于 \(O\),从而归纳证得贪心解必最优。
自学导航 · 动态规划与贪心:状态、边界和证明
① 前置知识与记号
先复习 min/max、下标与二维表。动态规划写题务必交代状态含义、转移方程、边界、填表顺序、复杂度。贪心必须用交换论证或其它正确性证明,不可只给直觉。
② 完整例题
0-1 背包:重量 [2,3,4],价值 [3,4,5],容量 5。选前两件得到最优价值 7。令 dp[i][c] 为前 i 件、容量上限 c 的最优值,转移取“不拿第 i 件”和“拿第 i 件”中的较大值,第二种要求 c≥wᵢ。时间 O(nC),当 C 按二进制编码时为伪多项式。
💡 破题关键与第一步提示(遮答案自测用 · ③ 考前自测(展开题目))
破题切入点:构造一个 0-1 背包按性价比贪心失败的例子。(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(③ 考前自测(展开题目))
构造一个 0-1 背包按性价比贪心失败的例子。
本栏是补充学习材料,考试范围以本学期教师正式通知为准。
考试演算:最长公共子序列(LCS)
已知:\(X=\text{ABCBDAB}\),\(Y=\text{BDCABA}\)。求 LCS 的长度,并写出状态转移与复杂度。
- 定义 \(dp[i][j]\):\(X\) 的前 \(i\) 个字符与 \(Y\) 的前 \(j\) 个字符的 LCS 长度。
- 边界:\(dp[0][j]=dp[i][0]=0\)。
- 若 \(X_i=Y_j\),则 \(dp[i][j]=dp[i-1][j-1]+1\);否则 \(dp[i][j]=\max(dp[i-1][j],dp[i][j-1])\)。
- 按行或按列填表,结果 \(dp[7][6]=4\),例如子序列 BCBA;时间 \(O(mn)\)、完整表空间 \(O(mn)\)。仅求长度可滚动数组降至 \(O(\min(m,n))\)。
💡 破题关键与第一步提示(遮答案自测用 · 自己做:贪心活动选择的交换论证)
破题切入点:以结束时间最早的活动 \(a\) 为第一项。(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(自己做:贪心活动选择的交换论证)
以结束时间最早的活动 \(a\) 为第一项。若某个最优安排第一项为 \(b\),由于 \(f_a\le f_b\),用 \(a\) 替换 \(b\) 后与后续活动仍兼容,数量不变,因此存在包含 \(a\) 的最优安排;再对子问题递归应用。注意适用条件是单资源、活动按不重叠兼容。
笔试与期末区分:Python 面试要会写二维/一维 DP;期末算法设计题先写状态、转移、初值与复杂度,再根据老师要求决定是否写伪代码。
先看:本模块易错点与补充
Python 实战- 0-1 背包一维滚动数组容量必须倒序,否则同一物品可能被重复选;O(nC) 属于伪多项式时间。
- 有权活动安排用动态规划,按结束时间贪心仅保证无权且求最多活动数量最优。
- 动手运行 LCS、背包与区间贪心实现,并对照 自动化测试。
0 · DP 思想与四步法
- 找出最优解的性质,刻画结构特征(证明最优子结构,常用反证法);
- 递归地定义最优值(写出递推关系式——大题的核心得分点);
- 自底向上计算最优值(填表,注明顺序与复杂度);
- 根据计算信息构造最优解(记录决策的辅助表回溯)。
- DP 大题:矩阵连乘 / LCS / 0-1 背包三选一(按四步法写:阶段→状态→递推方程→初值与求解顺序,缺步扣分);
- 贪心大题:活动安排(或带权活动,2021 大纲点名)+ 交换论证正确性证明(大纲原文要求掌握模板,见"正确性证明模板 ★★"框);
- 贪心 vs DP 之辨:背包问题贪心解法 + C=50 反例说明 0-1 背包不能用贪心——判断/简答高频;
- 多机调度 LPT——Review 问答题原题(算法描述 + 是否最优);
- 填空:DP 两要素、最优子结构/重叠子问题、备忘录与 DP 的关系。
数学手生?增长率比较(对数<幂<指数)、求和公式、矩阵运算的"三分钟复习"在上级目录 《数学基础工具箱》第 3、4、5 节;考前看第 8 节"急救包"。
1 · 矩阵连乘问题 ★
问题:给定 n 个可乘矩阵 \(A_1\cdots A_n\),加括号确定计算次序使数乘次数最少。矩阵 \(A_{p\times q}\times B_{q\times r}\) 耗费 \(pqr\)。
② 递推式:设 \(m[i][j]\)=计算 \(A[i:j]\) 的最少数乘次数, \[m[i][i]=0;\qquad m[i][j]=\min_{i\le k<j}\big\{m[i][k]+m[k+1][j]+p_{i-1}p_kp_j\big\}\quad(i<j)\] ③ 复杂度:子问题只有 \(\binom n2+ n=O(n^2)\) 个(重叠!),自底向上按链长 r=2…n 计算,三重循环 O(n³) 时间、O(n²) 空间。
💡 破题关键与第一步提示(遮答案自测用 · 填表口诀与 s 表)
破题切入点:按链长 r=2,3,… 逐条对角线填:先对角线 0,再长度 2(一次比较),长度 3(两次比较)……同时用 \(s[i][j]=k\) 记录断开点。(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(填表口诀与 s 表)
作业二同款:p=(10,100,5,50,30,20,60,45,50)(8 个矩阵),按同流程先填 m 表再写最优加括号方式,并画语法树/三角剖分图。
📄 期末禁止写代码:矩阵连乘问题(Matrix Chain Multiplication) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:设矩阵链 \(A_1 A_2 \dots A_n\),其中 \(A_i\) 维数为 \(p_{i-1}\times p_i\)。定义 \(m[i][j]\) 为计算子链 \(A_{i..j}\) 所需的最少标量乘法次数,\(s[i][j]\) 为取得最优值时的最后一次断开位置 \(k\)。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:边界条件:单一矩阵无需乘法,\(m[i][i]=0\)(\(1\le i\le n\));转移方程:当 \(i
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:初始化主对角线 \(m[i][i]=0\);第二步:按子链长度 \(l=2,3,\dots,n\) 从短到长沿着右上对角线逐层推进;第三步:对每个起点 \(i\)(终点 \(j=i+l-1\)),枚举分割点 \(k\in[i,j-1]\) 更新 \(m[i][j]\) 与 \(s[i][j]\);第四步:递归利用 \(s[1..n]\) 输出最优加括号方案。
- ④ 时间与空间复杂度严格推导:共 \(\Theta(n^2)\) 个子问题状态,每个状态需枚举 \(O(n)\) 个分割点 \(k\),总时间复杂度 \(\Theta(n^3)\),表格存储空间复杂度 \(\Theta(n^2)\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def matrix_chain_order(p):
"""矩阵连乘最小标量乘法次数,矩阵 A_i 维度为 p[i-1] x p[i]。O(n^3) 时间、O(n^2) 空间。"""
n = len(p) - 1
if n <= 1:
return 0
m = [[0] * (n + 1) for _ in range(n + 1)]
for length in range(2, n + 1): # 链长 l 从 2 到 n 对角线推进
for i in range(1, n - length + 2):
j = i + length - 1
m[i][j] = min(
m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j]
for k in range(i, j)
)
return m[1][n]
2 · 两大基本要素 + 备忘录
3 · 最长公共子序列 LCS ★
定义:子序列 = 删去若干元素后得到(不要求连续,与子串区分)。LCS:给定 \(X_{1..m},Y_{1..n}\),找最长的公共子序列(“一个”即可,不必唯一)。
📄 期末禁止写代码:最长公共子序列(LCS) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:设序列 \(X=\langle x_1,\dots,x_m\rangle\) 与 \(Y=\langle y_1,\dots,y_n\rangle\),定义 \(c[i][j]\) 为前缀 \(X_{1..i}\) 与 \(Y_{1..j}\) 的最长公共子序列长度。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:边界:\(c[i][0]=0, c[0][j]=0\);状态转移:若 \(x_i=y_j\) 则 \(c[i][j]=c[i-1][j-1]+1\);若 \(x_i\ne y_j\) 则 \(c[i][j]=\max(c[i-1][j], c[i][j-1])\)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:将第 0 行与第 0 列初始化为 0;第二步:外层循环 \(i=1\dots m\)、内层循环 \(j=1\dots n\),比较 \(x_i\) 与 \(y_j\) 按转移方程填表并记录方向标记(左上 `↖`、上 `↑`、左 `←`);第三步:从 \((m,n)\) 逆向追踪左上箭头输出 LCS 序列。
- ④ 时间与空间复杂度严格推导:填表共 \(m\times n\) 个单元格,每格转移耗时 \(O(1)\),时间复杂度 \(\Theta(mn)\);若仅求长度可用滚动数组将空间从 \(\Theta(mn)\) 降至 \(\Theta(\min(m,n))\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def lcs_length(a, b):
"""LCS 长度。O(len(a)*len(b)) 时间,O(len(b)) 空间。"""
prev = [0] * (len(b) + 1)
for ca in a:
dp = [0] * (len(b) + 1)
for j, cb in enumerate(b, 1):
dp[j] = prev[j - 1] + 1 if ca == cb else max(prev[j], dp[j - 1])
prev = dp
return prev[-1]
4 · 最大子段和(四算法对比)★
问题:给定含负数的序列,求 \(\max_{1\le i\le j\le n}\sum_{k=i}^{j}a_k\)(全负时定义为 0)。例:(−2,11,−4,13,−5,−2) → 最大子段和 20(11−4+13)。
| 算法 | 思路 | 复杂度 |
|---|---|---|
| 穷举 v1 | 三重循环:枚举 i,j 再累加求和 | O(n³) |
| 穷举 v2 | 枚举 i,j,j 递增时沿用部分和 | O(n²) |
| 分治 | 三分情况:左半 / 右半 / 跨中线(从中线向两侧扩展求最大);\(T(n)=2T(n/2)+O(n)\) | O(n log n) |
| DP ★ | \(b[j]=\max\{b[j-1]+a[j],\ a[j]\}\)(b[j]=以 j 结尾的最大子段和),答案 \(\max_j b[j]\) | O(n) |
📄 期末禁止写代码:最大子段和(动态规划 Kadane 算法) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:设数组 \(A[1..n]\),定义 \(b[j]\) 为“必须以元素 \(A[j]\) 结尾”的最大连续子段和,全局最优解为 \(\max_{1\le j\le n} b[j]\)。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:边界:\(b[1]=A[1]\);状态转移:\(b[j]=\max(b[j-1]+A[j],\, A[j])\)(即若前序子段和 \(b[j-1]>0\) 则接续,否则从 \(A[j]\) 重新起头)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:令当前尾子段和 \(cur=A[1]\)、全局最大值 \(best=A[1]\);第二步:从 \(j=2\) 到 \(n\) 顺序扫描,用 \(\max(cur+A[j], A[j])\) 更新 \(cur\),再用 \(\max(best, cur)\) 更新 \(best\);第三步:遍历结束返回 \(best\)。
- ④ 时间与空间复杂度严格推导:仅需单重线性扫描,时间复杂度 \(\Theta(n)\);状态仅依赖前一项,滚动变量空间复杂度 \(\Theta(1)\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def max_subarray(nums):
"""最大非空连续子段和(Kadane 算法)。O(n) 时间、O(1) 空间。"""
if not nums:
raise ValueError('数组不能为空')
cur = best = nums[0]
for x in nums[1:]:
cur = max(x, cur + x)
best = max(best, cur)
return best
5 · 凸多边形最优三角剖分
问题:凸多边形加不相交的弦剖成三角形(恰 n−3 条弦、n−2 个三角形),最小化三角形权之和 \(W\)(如周长)。
6 · 图像压缩
问题:灰度序列 \(p_1..p_n\)(0–255)。变位压缩:分成 m 段,每段用统一的 b[i] 位(该段最大灰度所需位数)存储,段头 3 位存 b[i]、8 位存段长(段长 ≤255)。求最优分段使总位数最少。
7 · 0-1 背包 ★(伪多项式考点)
问题:n 物品重量 \(w_i\) 价值 \(v_i\),容量 C,每物选/不选(xᵢ∈{0,1}),最大化总价值。\(\max\sum v_ix_i\) s.t. \(\sum w_ix_i\le C\)。穷举 O(2ⁿ);贪心不一定最优。
💡 破题关键与第一步提示(遮答案自测用 · 填表与最优解)
破题切入点:逐行迭代(每行 i 由上一行推出),得 V(5,10)= 15 。(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(填表与最优解)
📄 期末禁止写代码:0-1 背包问题(动态规划法) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:设共 \(n\) 件物品,重量 \(w_i\)、价值 \(v_i\),背包容量 \(C\)。定义 \(m[i][j]\) 为仅考虑前 \(i\) 件物品、可用容量不超过 \(j\) 时的最大总价值。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:边界:\(m[0][j]=0\),\(m[i][0]=0\);转移方程:当 \(j < w_i\) 时装不下,\(m[i][j]=m[i-1][j]\);当 \(j \ge w_i\) 时,\(m[i][j]=\max(m[i-1][j],\, m[i-1][j-w_i]+v_i)\)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:初始化容量表第 0 行为 0;第二步:依次枚举物品 \(i=1\dots n\) 及容量 \(j=1\dots C\)(一维空间优化时容量 \(j\) 必须从 \(C\) 倒序递减至 \(w_i\));第三步:从 \((n,C)\) 逆推比较 \(m[i][j]\) 与 \(m[i-1][j]\) 是否相等以构造 0-1 解向量。
- ④ 时间与空间复杂度严格推导:时间复杂度 \(\Theta(nC)\),空间复杂度 \(\Theta(nC)\)(仅求最优值可降为 \(\Theta(C)\))。注意 \(C\) 的数值大小关于输入编码长度 \(\log_2 C\) 呈指数级,故属伪多项式时间算法。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def knapsack_01(weights, values, capacity):
"""正整数重量的 0-1 背包。O(n*capacity) 伪多项式时间。"""
if (
len(weights) != len(values)
or not isinstance(capacity, int)
or capacity < 0
or any(not isinstance(w, int) or w <= 0 for w in weights)
):
raise ValueError('输入非法:重量须正整数、容量须非负整数')
dp = [0] * (capacity + 1)
for w, v in zip(weights, values):
for c in range(capacity, w - 1, -1): # 倒序防止一个物品被重复选
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]
8 · 最优二叉查找树
问题:有序集 \(k_1<\cdots<k_n\),查找成功概率 \(p_i\)、落在空隙的概率 \(q_i\)。构造 BST 最小化期望搜索代价 \(\sum(\text{depth}+1)\times\)概率。
- \(E[i][j]=\min_{i\le r\le j}\big\{E[i][r-1]+E[r+1][j]+W[i][j]\big\}\),边界 \(E[i][i-1]=q_{i-1}\);
- \(R[i][j]=r\) 记录最优根,用于 O(n) 构造树。
按对角线(子树大小 d)逐条计算:时间 O(n³)、空间 O(n²)。
作业二同款:5 个关键字的查找概率分布,建 E/W/R 三表并画最优 BST。
📄 期末禁止写代码:最优二叉查找树(Optimal BST) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:给定有序关键字 \(k_1<\dots
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:边界:空实结点子树仅含单个伪结点,\(e[i][i-1]=w[i][i-1]=q_{i-1}\)(\(1\le i\le n+1\));转移:子树整体下沉一层使每个结点深度 +1,故 \(w[i][j]=w[i][j-1]+p_j+q_j\),\(e[i][j]=\min_{i\le r\le j}\{e[i][r-1]+e[r+1][j]+w[i][j]\}\)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:初始化主对角线下方 \(j=i-1\) 处的 \(e\) 与 \(w\) 为 \(q_{i-1}\);第二步:按关键字个数 \(l=1,2,\dots,n\) 沿对角线推进;第三步:对每个区间 \([i,j]\) 先 O(1) 递推算 \(w[i][j]\),再枚举根结点 \(r\in[i,j]\) 取最小期望代价并记录根表 \(root[i][j]=r\)。
- ④ 时间与空间复杂度严格推导:时间复杂度 \(\Theta(n^3)\)(若利用 Knuth 四边形不等式单调性 \(root[i][j-1]\le root[i][j]\le root[i+1][j]\) 可优化至 \(\Theta(n^2)\)),空间复杂度 \(\Theta(n^2)\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def optimal_bst(p, q):
"""最优二叉查找树期望代价,p[0..n-1] 为实结点概率,q[0..n] 为伪结点概率。O(n^3) 时间。"""
n = len(p)
e = [[0.0] * (n + 2) for _ in range(n + 2)]
w = [[0.0] * (n + 2) for _ in range(n + 2)]
for i in range(1, n + 2):
e[i][i - 1] = w[i][i - 1] = q[i - 1]
for length in range(1, n + 1):
for i in range(1, n - length + 2):
j = i + length - 1
w[i][j] = w[i][j - 1] + p[j - 1] + q[j]
e[i][j] = min(
e[i][r - 1] + e[r + 1][j] + w[i][j]
for r in range(i, j + 1)
)
return e[1][n]
9 · 贪心思想与两要素
10 · 活动安排 + 正确性证明 ★★
问题:n 个活动竞争同一资源,活动 i 有 \([s_i,f_i]\)。相容 = 时间不重叠。求最大相容活动子集。
💡 破题关键与第一步提示(遮答案自测用 · 完整证明(照抄即得分))
破题切入点:贪心选择性质 (存在以贪心选择开始的最优解):设 E 按结束时间排序,活动 1 结束最早。(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(完整证明(照抄即得分))
贪心选择性质(存在以贪心选择开始的最优解):设 E 按结束时间排序,活动 1 结束最早。对任意最优解 A:若 1∈A 则完成;否则取 A 中结束最早的活动 j,用 1 替换 j 得 A′=(A−{j})∪{1}。因 \(f_1\le f_j\),A′ 中活动仍两两相容,且 |A′|=|A| ⟹ A′ 也是最优解且含活动 1。
最优子结构 + 归纳:选 1 后原问题化为子问题 E′={i∈E : sᵢ≥f₁} 的活动安排;A′=A−{1} 必是 E′ 的最优解(否则有更优 A″,则 A″∪{1} 比原 A 更优,矛盾)。
交换论证(大纲 S15 给出的通用模板):假设贪心解 S₁ 不是最优;取与 S₁ 重合元素最多的最优解 S₂;比较 S₁ 与 S₂ 第一个不同的元素,把 S₂ 中该元素换成 S₁ 的对应元素得 S₃——S₃ 仍是最优解且与 S₁ 重合更多,与 S₂ 的取法矛盾。故 S₁ 最优。∎ (==所有贪心证明题都套这个骨架==:替换论证 + 归纳。)
📄 期末禁止写代码:活动安排(贪心法)与带权活动安排(DP 对照) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:无权活动安排:给定区间集合 \(\{[s_i, f_i)\}\),求最大相容子集;带权活动安排:每个活动附带权重 \(v_i\),定义 \(p(i)\) 为在活动 \(i\) 开始前已结束的最后一个活动下标,\(dp[i]\) 为前 \(i\) 个活动的最大总权重。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:无权贪心策略:按结束时间 \(f_1\le f_2\le\dots\le f_n\) 升序排列,每次贪心选取结束最早且与已选活动相容(\(s_i \ge f_{last}\))的活动(交换论证法可证贪心选择性质);带权活动 DP 方程:\(dp[i]=\max(dp[i-1],\, dp[p(i)]+v_i)\)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):无权贪心四步:1) 将活动按结束时间非降序排序;2) 选入第 1 个活动并记录其结束时间 \(last\);3) 顺序扫描其余活动,凡满足 \(s_i\ge last\) 者立即选入并更新 \(last=f_i\);4) 返回所选集合。
- ④ 时间与空间复杂度严格推导:无权贪心:排序耗时 \(O(n\log n)\),线性扫描耗时 \(O(n)\),总时间 \(O(n\log n)\)、空间 \(O(1)\);带权 DP:排序 + 二分查找 \(p(i)\) + 线性递推,总时间 \(O(n\log n)\)、空间 \(O(n)\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def activity_selection(intervals):
"""最多不重叠区间 [start,end)。O(n log n) 时间。"""
if any(t < s for s, t in intervals):
raise ValueError('结束早于开始')
ans, last = [], float('-inf')
for s, t in sorted(intervals, key=lambda x: x[1]):
if s >= last:
ans.append((s, t))
last = t
return ans
def weighted_interval_scheduling(intervals):
"""带权活动安排 (start, end, weight) 最大总权重。O(n log n) 时间。"""
if not intervals:
return 0
jobs = sorted(intervals, key=lambda x: x[1])
ends = [j[1] for j in jobs]
dp = [0] * (len(jobs) + 1)
for i, (s, _, w) in enumerate(jobs, 1):
p = bisect_right(ends, s, 0, i - 1)
dp[i] = max(dp[i - 1], dp[p] + w)
return dp[-1]
带权活动安排:2021 历史复习大纲要求的完整 DP 演算
令活动区间采用 [start,finish),结束时刻等于下一活动开始时刻视为兼容。各活动按结束时间升序排列:
| 编号 | 开始 | 结束 | 收益 v | p(j) | dp[j] |
|---|---|---|---|---|---|
| A | 1 | 3 | 5 | 0 | 5 |
| B | 2 | 5 | 6 | 0 | 6 |
| C | 4 | 6 | 5 | A | 10 |
| D | 6 | 7 | 4 | C | 14 |
| E | 5 | 8 | 11 | B | 17 |
| F | 7 | 9 | 2 | D | 17 |
- 定义 \(p(j)=\max\{i<j:f_i\le s_j\}\),若不存在,则记为 0;可以按结束时间二分查找得到。
- 定义 \(dp[j]\) 为前 j 个活动的最大收益,\(dp[0]=0\);转移 \(dp[j]=\max(dp[j-1],v_j+dp[p(j)])\)。
- 以 E 为例:不选 E 时最好 14;选 E 收益 \(11+dp[B]=11+6=17\),故 \(dp[E]=17\)。
- 最终最优值 17。回溯:F 不优于 E → 取 E → 跳到
p(E)=B→ 取 B,因此最优活动集为 {B,E}。
正确性思路:任意最优方案要么不含 j(最佳收益 \(dp[j-1]\)),要么含 j(此前只能选编号 ≤p(j) 的活动,最佳收益 \(v_j+dp[p(j)]\)),两种情况穷尽。排序与求前驱 O(n log n),逐项填表 O(n),总时间 O(n log n)。
💡 破题关键与第一步提示(遮答案自测用 · 自测:为什么不能直接选择收益最大的活动 E?)
破题切入点:只有在此例中恰好能选到最优方案中的 E;(👉 先合上下方完整推导,自己动手算完关键步再核对踩分点!)
📝 完整规范推导与评分踩分点(自测:为什么不能直接选择收益最大的活动 E?)
只有在此例中恰好能选到最优方案中的 E;一般不能只依据单件最高收益判断最优。必须考虑兼容活动的组合,例如一个收益 10 的长活动可能不如两件互不冲突、各收益 6 的短活动。
11 · 背包 vs 0-1 背包:贪心的分界线 ★★
- 背包(可拆):装 1、2 全部 + 3 的 2/3 → 60+100+80=240 最优 ✓
- 0-1 背包贪心:装 1、2 → 160;而最优是物品 2+3 = 220 ✗
12 · 最优装载 / Dijkstra / 多机调度
12.1 最优装载(贪心可证)
尽量多装集装箱(不超载重 c)。策略:从轻到重依次装。贪心选择性质证明:设最优解 X 的最轻入选箱为 k(k≠1),用 1 替换 k 得 X′ 仍可行且数量不变——存在含最轻箱的最优解;归纳收尾。O(n log n)(排序)。
12.2 Dijkstra 单源最短路径(贪心)
12.3 多机调度(NP 难 + 贪心近似)
例(7 作业 {2,14,4,16,6,5,3},3 台):排序 {16,14,6,5,4,3,2}:M1←16,M2←14,M3←6;5→M3(11),4→M3(15),3→M2(17),2→M3(17)。完工时间 17(M1:J4=16;M2:J2+J7=17;M3:J5+J6+J3+J1=17)。∎
| 分治 | DP | 贪心 | |
|---|---|---|---|
| 子问题 | 独立 | 重叠 | 只走一条路 |
| 解的构造 | 合并子解 | 自底向上填表 + 回溯构造 | 自顶向下逐步构造 |
| 关键性质 | 可合并性 | 最优子结构 + 重叠子问题 | 贪心选择性质 + 最优子结构 |
| 典型 | 归并/快排/Strassen | 矩阵连乘/LCS/0-1背包 | 活动安排/Dijkstra/背包可拆 |
📄 期末禁止写代码:Dijkstra 单源最短路径算法(贪心法) · 自然语言标准作答四步模板
- ① 状态/子问题定义与符号声明:设非负权有向图 \(G=(V,E,w)\)(\(w(u,v)\ge 0\)),源点 \(s\)。维护已确定最短路顶点集 \(S\),以及每个顶点 \(v\) 当前已知的从 \(s\) 出发最短距离估计上界 \(dist[v]\)。
- ② 核心递推方程 / 贪心策略 / 搜索剪枝准则:贪心选择策略:每轮从 \(V\setminus S\) 中选出 \(dist[u]\) 最小的顶点 \(u\) 加入 \(S\)(因边权非负,绕道其它未访问点不可能更短);松弛操作:对 \(u\) 的每条出边 \((u,v)\),用 \(\min(dist[v],\, dist[u]+w(u,v))\) 更新 \(dist[v]\)。
- ③ 算法自然语言分步流程(严禁贴 C/Python 语法):第一步:初始化 \(dist[s]=0\),其余 \(dist[v]=+\infty\),将 \((0,s)\) 放入最小优先队列;第二步:弹出队首距离最小且未入 \(S\) 的顶点 \(u\) 并标记已确定;第三步:遍历 \(u\) 的所有邻接点 \(v\) 执行松弛,若 \(dist[v]\) 变小则压入优先队列;第四步:队列为空时输出 \(dist\) 数组。
- ④ 时间与空间复杂度严格推导:二叉堆实现下,共最多 \(O(V)\) 次有效出队与 \(O(E)\) 次松弛入队,总时间复杂度 \(O((V+E)\log V)\)(稠密图用朴素数组扫描为 \(O(V^2)\)),空间复杂度 \(O(V+E)\)。
🐍 Python 求职机试标准实现(源自 practice_algorithms.html · 在线阅读器)
def dijkstra(graph, source):
"""邻接表 (v,weight) 的非负最短路。堆 O((V+E)log V)。"""
n = len(graph)
if not 0 <= source < n:
raise ValueError('源点越界')
if any(v < 0 or v >= n or w < 0 for edges in graph for v, w in edges):
raise ValueError('端点越界或存在负权')
dist = [float('inf')] * n
dist[source] = 0
pq = [(0, source)]
while pq:
d, u = heappop(pq)
if d > dist[u]:
continue
for v, w in graph[u]:
new = d + w
if new < dist[v]:
dist[v] = new
heappush(pq, (new, v))
return dist
13 · 公式速查(一页纸素材)
| 矩阵连乘 | \(m[i][j]=\min_k\{m[i][k]+m[k+1][j]+p_{i-1}p_kp_j\}\);O(n³);穷举=卡特兰数 |
| LCS | 相等:c[i−1][j−1]+1;不等:max(c[i][j−1], c[i−1][j]);O(mn) |
| 最大子段和 | \(b[j]=\max\{b[j-1]+a[j],a[j]\}\);O(n) |
| 三角剖分 | 与矩阵连乘同构;换权函数 W(△vᵢ₋₁v_kvⱼ) |
| 图像压缩 | \(S[i]=\min_j\{S[i-j]+j\,b_{\max}+11\}\),段长≤256;O(n) |
| 0-1 背包 | \(V(i,j)=\max\{V(i-1,j),V(i-1,j-w_i)+v_i\}\);O(nC) 伪多项式;构造解逆推 |
| 最优 BST | \(E[i][j]=\min_r\{E[i][r-1]+E[r+1][j]\}+W[i][j]\);O(n³) |
| 贪心两要素 | 贪心选择性质 + 最优子结构;证明 = 替换/交换论证 + 归纳 |
| 贪心经典 | 活动安排按结束时间排序 O(n);背包按 v/w 排序 O(nlogn);装载轻者先;Dijkstra O(n²);多机调度 LPT |
14 · 自测(含作业与真题)
- 动态规划与分治的基本思想相同,主要区别在于子问题是否重叠且需保存子问题的解。(√——Review 填空原题:分治重复计算,DP 查表避免)
- 贪心算法总能得到全局最优解。(×)
- 0-1 背包问题可以用贪心算法求得最优解。(×;可拆背包可以)
- 0-1 背包的 DP 算法是多项式时间算法。(×——O(nC) 为伪多项式,大纲原话)
- 备忘录方法是 DP 的变形,采用自顶向下的递归并保存已解子问题。(√)
- 矩阵链 p=(10,100,5,50,30,20,60,45,50):写最优加括号方式与语法树(作业二原题——先填 m 表)。
- 求 X={A,B,C,B,D,A,B} 与 Y={B,D,C,A,B,A} 的 LCS 并给 c 表。(答:长度 4)
- 序列 (−2,11,−4,13,−5,−2):给出 DP 求 b 数组全过程。(答:b={−2,11,7,20,15,13},最大 20)
- n=5, w={2,2,6,5,4}, v={6,3,5,4,6}, C=10:填 V 表并给最优解。(答:x=(1,1,0,0,1),15)
- 哈夫曼编码:频率 31%,12%,5%,2%,10%,18%,22% 的字符 a–g,画哈夫曼树、给编码、算平均码长。(作业三选做)
- 证明:按结束时间贪心的活动安排是最优的。(照抄 §10 模板)
🎯 考场题型信号词 → 破题套路速查表 期末冲刺必背 · 信号词秒配定理与防扣分清单
考场上读题 10 秒内锁定“题干信号词”,直接调用中间列的定理模板作答,并在交卷前对照右列逐条排查高频扣分点。
| 🔍 题干信号词 / 设问特征 | ⚡ 必用定理 / 核心递推式 / 标准破题套路 | 🚨 老师批改高频扣分点 |
|---|---|---|
| 矩阵连乘 / 凸多边形三角剖分 / 最优二叉查找树(区间 DP 大题) | 状态 \(m[i][j]\) 设为子区间 \([i,j]\) 的最优值,按区间长度 \(l=j-i+1\) 从小到大沿对角线填表,枚举分割点/根结点 \(k\in[i,j]\) | ⚠ 避坑:填表循环写成普通双重循环 `i=1..n, j=i..n`(此时 \(m[k+1][j]\) 尚未算出!必须最外层枚举链长 \(l\));最优 BST 漏加虚伪结点概率 \(q_i\) |
| 双序列匹配(LCS / 编辑距离)或 0-1 背包 DP 设计与填表追踪 | 先写出完整四要素:① 最优子结构证明(反证/剪切粘贴法);② 状态定义;③ 含边界条件的分段递推式;④ 填表顺序与逆推构造最优解过程 | ⚠ 避坑:只写递推式漏写边界条件(如 \(c[i][0]=0\));只求最优值漏写如何利用辅助表 \(s/b\) 构造最优解方案 |
| 判断背包问题用贪心还是 DP,或解释 0-1 背包为何是 NP 完全却有 \(O(nC)\) 算法 | 分数背包可切分具贪心选择性质(按 \(v_i/w_i\) 降序 \(O(n\log n)\));0-1 背包不具贪心性质(尾部空隙浪费,必须用 DP)。\(O(nC)\) 关于容量编码位数 \(L=\log_2 C\) 是 \(O(n 2^L)\),属伪多项式时间 | ⚠ 避坑:把 \(O(nC)\) 当作真正的多项式时间算法从而误判 \(\text{P}=\text{NP}\) |
| 活动安排 / 最优装载 / 哈夫曼编码 / 单源最短路证明贪心算法正确性 | 两大证明支柱:① 贪心选择性质(用“交换论证法 Exchange Argument”:设存在最优解 \(O\),用贪心首项 \(g_1\) 替换 \(O\) 中首项得 \(O'\),证 \(O'\) 合法且不劣于 \(O\));② 最优子结构性质 | ⚠ 避坑:证明贪心选择性质时只举数字例子或空喊口号,未写出“设最优解为 \(A\),构造替换解 \(A'=(A\setminus\{k\})\cup\{1\}\)”的标准数学归纳过程 |
| 带权区间调度(Weighted Interval Scheduling)能否直接按结束时间贪心? | 不能!带权活动安排贪心失效,必须先按结束时间排序预处理出每个活动前最后相容活动 \(p(i)\),再用 DP 方程 \(dp[i]=\max(dp[i-1], dp[p(i)]+v_i)\) 在 \(O(n\log n)\) 求解 | ⚠ 避坑:审题不细,把带权重的活动安排当成普通无权活动安排直接用贪心法作答 |