跳至主要內容

第7次内容

周子力大约 8 分钟教学文档算法设计与分析

1.上次内容-动态规划

好的,以下是一段适合作为下一次算法课引入部分的讲稿(风格兼具教学逻辑与课堂互动性):


💡课程引入:线性DP专题回顾与深化

同学们,上节课我们一起分析了三个非常典型的动态规划(Dynamic Programming)问题:

  • “买包子”问题 —— 我们从组合思维出发,探讨了如何用最少的笼数或者判断哪些数量买不到;
  • “买不到的数字”问题 —— 我们利用DP判断哪些数能被线性组合得到,揭示了线性DP的可达性特征;
  • “买卖股票的最佳时机”问题 —— 我们进一步思考了如何在状态转移中引入“历史最优”,实现收益最大化。

虽然这三个题目看起来背景各不相同,一个是包子铺的日常,一个是数学的不可达问题,一个是金融市场的收益决策,但它们都体现出一个共同的算法思想—— 👉 线性动态规划(Linear DP)


🧭 一、线性DP的特征

线性DP的核心特征是:

  1. 状态具有线性顺序:状态往往按照时间、序列、或数量的顺序进行计算,如从 1n 的连续状态。
  2. 转移方向单一:每个状态只依赖前面若干个状态,如 dp[i]dp[i-1]dp[i-k] 推出。
  3. 子问题无重叠计算冲突:每个状态都可以通过固定规则由之前的状态唯一确定。
  4. 常见应用场景:最大值、最小值、可达性、计数类问题等。

🧩 二、线性DP的解题步骤

我们在解决这类问题时,可以遵循一套通用流程:

  1. 确定状态定义:明确 dp[i] 表示什么(例如“到第 i 天的最大收益”或“能否买到 i 个包子”);
  2. 寻找状态转移方程:分析第 i 个状态和前面状态之间的关系(如 dp[i] = max(dp[i-1], prices[i] - min_price));
  3. 设置边界条件:如 dp[0] 的初值,或不可达状态的初始化;
  4. 按顺序迭代求解:从小到大推进,避免重复计算;
  5. 输出结果:根据题意输出最终答案(如最大值、最小值或可行性)。

⚠️ 三、需要注意的问题

  1. 状态定义必须清晰且可递推,模糊的定义会导致方程混乱;
  2. 初始化极其重要,尤其是表示“不可达”“未发生”的情况;
  3. 边界判断不能漏,特别是在i-1i-k可能越界时;
  4. 时间复杂度控制,线性DP要保持O(n)O(n*k)级别;
  5. 思维要有方向感,搞清楚“自底向上”还是“自顶向下(记忆化搜索)”。

今天我们将在上节内容的基础上,继续拓展线性DP的思维。我们会看看:

  • 如何将线性DP扩展到区间DP背包DP
  • 如何通过状态压缩优化转移让算法更加高效。

二、区间DP

线性DP的扩展,就是将状态定义为区间,而不是单个元素。例如,给定一个数组,求区间和,或者区间最大值,等等。

1. 区间DP的定义

区间DP(Interval DP)是一种动态规划问题,其状态定义为某个区间内的最优解。例如,对于一个数组arr[1...n],我们定义一个二维的DP表dp[i][j]表示从第i个元素到第j个元素的某个最优解。 状态转移方程:dp[i][j] = min(dp[i][j-1], dp[i+1][j], dp[i+2][j-1], ..., dp[i+k-1][j-k+1]),其中k是区间长度。

2. 区间DP的解题步骤

(1) 确定状态定义:区间DP的状态定义为某个区间内最优解。 (2) 寻找状态转移方程:根据问题的具体要求,确定如何从子区间转移到当前区间的最优解。 (3) 设置边界条件:例如,对于空区间或单个元素的情况。 (4) 按顺序迭代求解:从小到大推进,避免重复计算。 (5) 输出结果:根据题意输出最终答案。 (6) 思考:区间DP的解题步骤与线性DP的解题步骤相似,但状态定义和转移方程不同。

3. 区间DP的例子

(1)矩阵连乘问题

(2)石子合并问题

非常好的问题 👍 区间DP(Interval DP) 是动态规划中进阶且极具代表性的一类,难点不在代码,而在状态设计与枚举策略的把握。 下面我系统性地总结一下求解区间DP时需要特别注意的关键点,可直接用于课堂讲解或讲义内容。


🧭 一、区间DP的基本特征回顾

区间DP的核心思想是:

“把一个区间划分为若干子区间,通过合并子区间的最优解来得到整个区间的最优解。”

常见于:

  • 石子合并 / 文件合并
  • 矩阵连乘
  • 括号匹配 / 表达式求值
  • 区间合并代价最小化

典型状态定义: [ dp[l][r] = \text{表示区间 } [l, r] \text{ 的最优值} ] 转移时通常通过枚举分割点 ( k ): [ dp[l][r] = \min_{l \le k < r} { dp[l][k] + dp[k+1][r] + \text{merge_cost}(l,r) } ]


⚠️ 三、求解区间DP时要注意的关键问题

1️⃣ 状态定义要准确、可递推

  • 明确 dp[l][r] 表示的意义,是“区间最优值”“最小代价”还是“最大收益”。
  • 不能出现含糊或递推不闭合的定义(如dp[l][r]依赖比[l,r]更大的区间)。

📌 课堂示例: 在石子合并问题中,dp[l][r] 表示合并区间 [l, r] 所需的最小代价,而不是合并到某个位置的代价,否则状态无法闭合。


2️⃣ 枚举顺序要正确(由小到大)

由于区间DP依赖更小的子区间结果,必须按照区间长度从小到大的顺序进行计算:

for len in range(2, n+1):      # 枚举区间长度
    for l in range(1, n-len+2):
        r = l + len - 1
        for k in range(l, r):  # 枚举分割点
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r] + cost)

📌 错误常见点:如果不按长度递推,会在转移时引用未计算的子区间。


3️⃣ 边界条件要清晰

通常区间最小长度为 1 的状态是边界: [ dp[i][i] = 0 \text{ 或 } \text{value[i]} ] 具体取决于问题定义(如单个元素不需要合并时为0)。


4️⃣ 分割策略要完整、无遗漏

在转移时必须遍历所有可能的分割点 k,否则会漏掉最优解。 同时要确保分割不越界:l ≤ k < r

📌 技巧: 部分题目可利用性质优化分割枚举(如 Knuth优化四边形不等式优化),但前提是理解清楚完整枚举过程。


5️⃣ 合并代价的计算要与区间一致

许多区间DP题目不仅取决于子区间的最优值,还取决于合并代价,如: [ dp[l][r] = dp[l][k] + dp[k+1][r] + (sum[r] - sum[l-1]) ] 这里的 (sum[r] - sum[l-1]) 必须对应整个区间的代价,不能写成局部的。

📌 常见错误:误写成 sum[k] - sum[l-1]sum[r] - sum[k],会导致答案偏小或偏大。


6️⃣ 时间复杂度控制

  • 朴素区间DP复杂度一般为 O(n³)

  • n 较大(如 1000),必须考虑优化策略,如:

    • Knuth优化:适用于满足“单调决策”条件的DP;
    • 四边形不等式优化
    • 分治优化

📌 教学提示:可让学生比较石子合并 O(n³) 与 Knuth 优化 O(n²) 的效果。


7️⃣ 思维模型:区间合并 → 树形结构

很多区间DP问题本质上是**“如何构造一棵最优的合并树”**。 理解这一点可以帮助学生更直观地写出转移式。

📌 例如:

  • 矩阵连乘:选择括号分割点;
  • 石子合并:选择合并顺序;
  • 表达式求值:选择运算符顺序。

本质都是“如何在一个序列上以最低代价合成一个整体”。


✅ 三、小结表格

注意点含义典型错误
状态定义必须可递推且闭合定义含糊导致无法递推
枚举顺序按区间长度递推反向计算引用未定义状态
边界条件初始化单点状态忘记 dp[i][i]
枚举完整性遍历所有分割点漏枚举或越界
合并代价与区间一致使用错误的 sum 区间
复杂度控制O(n³)→O(n²) 优化无法通过大数据测试
抽象思维“区间合并 = 树结构”只会写方程,不理解结构

上次编辑于:
贡献者: zhouzili,zilizhou