Files
final-exam/算法设计与分析/复习文档/动态规划.md
T

237 lines
8.2 KiB
Markdown
Raw Normal View History

2026-06-10 11:17:10 +08:00
---
tags:
- 算法设计与分析
- 复习
- 动态规划
create time: 2026-06-10 11:11
---
# 动态规划
## 概述
动态规划(Dynamic Programming, DP)是解决多阶段决策最优化问题的重要方法。与贪心算法的"不回头"不同,动态规划通过系统地考察所有可能的子问题解,利用存储机制避免重复计算,从而高效地求得全局最优解。本文将从基本思想出发,讲解动态规划的五个步骤和多个经典应用。
## 正文
### 一、动态规划基本思想
动态规划的核心思想是:**通过存储子问题的解,避免重复计算,将优化问题转化为决策过程。**
```mermaid
flowchart TD
Analyze["Step 1: Analyze optimal substructure"]
State["Step 2: Define state (subproblems)"]
Equation["Step 3: Write state transition equation"]
Compute["Step 4: Compute bottom-up (fill table)"]
Construct["Step 5: Construct optimal solution"]
Analyze --> State
State --> Equation
Equation --> Compute
Compute --> Construct
```
### 二、两个关键性质
1. **最优子结构(Optimal Substructure)**:问题的最优解包含子问题的最优解。可以通过组合子问题的最优解得到原问题的最优解。
2. **重叠子问题(Overlapping Subproblems)**:递归求解时,相同的子问题被反复计算。动态规划通过存储已解决的子问题解来避免重复计算。
> [!question] 动态规划的两个关键性质是什么?
> 最优子结构和重叠子问题。最优子结构说明子问题的最优解可以组合成全局最优解;重叠子问题说明存在重复计算,需要存储子问题的解。
### 三、动态规划五个基本步骤
这是解决动态规划问题的标准流程:
1. **分析最优子结构**:证明原问题的最优解包含子问题的最优解
2. **定义状态(子问题)**:明确 dp[i] 或 dp[i][j] 代表什么含义
3. **写出状态转移方程**:dp[i] 如何由之前的状态推导而来
4. **自底向上计算(填表)**:从小规模子问题开始,逐步填表
5. **构造最优解**:根据dp表回溯得到具体的最优解方案(不总是需要)
> [!warning] 状态定义是最关键的一步
> 动态规划的难点往往在于正确地定义状态。状态必须能完整描述一个子问题,且能通过之前的状态推导出来。
### 四、与分治法的区别
| 特性 | 分治法 | 动态规划 |
|------|--------|---------|
| 子问题关系 | 相互独立、无重叠 | 有重叠子问题 |
| 是否需要存储 | 不需要(子问题只算一次) | 需要(避免重复计算) |
| 计算方向 | 自顶向下递归 | 自底向上填表(也可记忆化递归) |
| 典型应用 | 归并排序、快速排序 | 0-1背包、LCS、矩阵链乘法 |
### 五、经典应用
#### 5.1 最大子数组和(Kadane算法)
**问题**:给定一个整数数组,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
**状态定义**:dp[i] = 以第 i 个元素结尾的最大子数组和
**状态转移方程**:
$$dp[i] = \max(arr[i], \ dp[i-1] + arr[i])$$
**含义**:以第 i 个元素结尾的最大子数组和,要么是从头开始(只取 arr[i]),要么是接在之前的最大子数组后面。
```plaintext
function maxSubarraySum(arr, n):
dp = array of size n
dp[0] = arr[0]
maxSum = dp[0]
for i = 1 to n-1:
dp[i] = max(arr[i], dp[i-1] + arr[i])
maxSum = max(maxSum, dp[i])
return maxSum
```
**解释**:对每个位置 i,我们做一个决策——是从当前位置重新开始子数组,还是接在前面的子数组后面。选择较大的那个作为 dp[i]。最后取所有 dp[i] 的最大值。
> [!question] Kadane算法的时间复杂度是多少?为什么?
> O(n),因为只需要一次遍历。对每个元素做常数次比较和赋值操作。
#### 5.2 n x n 矩阵最大路径和
**问题**:给定一个 n x n 的矩阵,从左上角走到右下角,每次只能向右或向下移动一步,求路径上的最大数字和。
**状态定义**:dp[i][j] = 从左上角到位置 (i,j) 的最大路径和
**状态转移方程**:
$$dp[i][j] = \max(dp[i-1][j], \ dp[i][j-1]) + matrix[i][j]$$
```plaintext
function maxMatrixPath(matrix, n):
dp = n x n matrix
dp[0][0] = matrix[0][0]
// 填第一行(只能从左边来)
for j = 1 to n-1:
dp[0][j] = dp[0][j-1] + matrix[0][j]
// 填第一列(只能从上面来)
for i = 1 to n-1:
dp[i][0] = dp[i-1][0] + matrix[i][0]
// 填其余位置
for i = 1 to n-1:
for j = 1 to n-1:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + matrix[i][j]
return dp[n-1][n-1]
```
**解释**:到达位置 (i,j) 只能从上方 (i-1,j) 或左方 (i,j-1) 来。取两者中较大的路径和,加上当前位置的值。边界条件是第一行和第一列只有一条路径。
#### 5.3 阶梯问题/爬楼梯
**问题**:有 n 阶楼梯,每次可以爬 1 阶或 2 阶,求到达第 n 阶有多少种方法。
**状态转移方程**:
$$f(n) = f(n-1) + f(n-2)$$
**解释**:到达第 n 阶的方法数 = 从第 n-1 阶走1步 + 从第 n-2 阶走2步。这是一个变体的 Fibonacci 数列。
```plaintext
function climbStairs(n):
if n <= 2: return n
dp = array of size n+1
dp[1] = 1
dp[2] = 2
for i = 3 to n:
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
```
> [!question] 爬楼梯和Fibonacci有什么关系?
> 爬楼梯的状态转移方程与Fibonacci完全相同:f(n) = f(n-1) + f(n-2)。区别在于初始值不同:爬楼梯 f(1)=1, f(2)=2;Fibonacci F(0)=0, F(1)=1。所以爬楼梯本质上就是Fibonacci数列的平移。
#### 5.4 斐波那契数列(动态规划解法)
用动态规划可以将 Fibonacci 从 O(2ⁿ) 优化到 O(n):
```plaintext
function fibDP(n):
if n <= 1: return n
dp = array of size n+1
dp[0] = 0
dp[1] = 1
for i = 2 to n:
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
```
> [!tip] 进一步优化空间
> 观察发现 dp[i] 只依赖 dp[i-1] 和 dp[i-2],因此不需要整个数组,只需要两个变量。空间复杂度可从 O(n) 降到 O(1)。
#### 5.5 最长公共子序列(LCS)
**问题**:给定两个序列 X 和 Y,找到它们的最长公共子序列。
**状态定义**:dp[i][j] = X 的前 i 个字符和 Y 的前 j 个字符的 LCS 长度
**状态转移方程**:
$$dp[i][j] = \begin{cases} dp[i-1][j-1] + 1 & \text{if } X[i] = Y[j] \\ \max(dp[i-1][j], \ dp[i][j-1]) & \text{if } X[i] \neq Y[j] \end{cases}$$
```plaintext
function LCS(X, Y, m, n):
dp = (m+1) x (n+1) matrix, all zeros
for i = 1 to m:
for j = 1 to n:
if X[i] == Y[j]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
```
**解释**:如果当前字符匹配,则 LCS 长度等于去掉这两个字符后的 LCS 长度加 1;如果不匹配,则取"去掉 X 的当前字符"和"去掉 Y 的当前字符"两种情况的较大值。
#### 5.6 矩阵链乘法
**问题**:给定 n 个矩阵的链 A₁A₂...Aₙ,找到使标量乘法次数最少的加括号方式。
**状态定义**:dp[i][j] = 计算矩阵 Aᵢ 到 Aⱼ 的最少乘法次数
**状态转移方程**:
$$dp[i][j] = \min_{i \le k < j} \{ dp[i][k] + dp[k+1][j] + p_{i-1} \cdot p_k \cdot p_j \}$$
这是动态规划的经典应用,时间复杂度 O(n³)。
### 六、动态规划作为优化决策过程
> [!tip] 动态规划的本质
> 动态规划本质上是将一个**优化决策过程**分解为多个阶段,每个阶段做一次决策。通过存储每个阶段每个状态下的最优解,最终得到全局最优决策序列。
```mermaid
flowchart LR
Init["Initial State S0"]
Decision1["Decision d1"]
State1["State S1"]
Decision2["Decision d2"]
State2["State S2"]
Dots["..."]
DecisionN["Decision dn"]
Final["Final State Sn"]
Init --> Decision1
Decision1 --> State1
State1 --> Decision2
Decision2 --> State2
State2 --> Dots
Dots --> DecisionN
DecisionN --> Final
```
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]