动态规划学习笔记
动态规划学习笔记
一、动态规划概述
动态规划(Dynamic Programming,简称 DP)是一种将复杂问题分解为子问题求解的算法思想。其核心特征是:当前阶段的状态由上一阶段状态和决策决定。
核心要素
| 要素 | 说明 |
|---|---|
| dp数组 | 可以是一维,也可以是二维,用来表示最优解 |
| 状态定义 | 确认 dp 数组表示什么含义 |
| 状态转移方程 | 写出状态之间的递推关系 |
| 初始值 | 确定边界条件的值 |
| 填表顺序 | 确定计算的先后顺序 |
动态规划解题步骤
- 确认 dp 数组表示什么 — 明确 dp[i] 或 dp[i][j] 的含义
- 写出状态转移方程 — 找出状态之间的递推关系
- 确定初始值 — 给边界条件赋值
- 确定遍历顺序 — 决定是从前往后还是从后往前
二、爬楼梯问题
问题描述
假设你正在爬楼梯,需要 n 阶才能到达楼顶。每次你可以爬 1 或 2 个台阶。问有多少种不同的方法可以爬到楼顶?
状态定义
dp[n] 表示爬到第 n 阶台阶的方法数。
状态转移方程
dp[n] = dp[n-1] + dp[n-2]
爬到第 n 阶可以从第 n-1 阶爬 1 步上来,也可以从第 n-2 阶爬 2 步上来。
初始值
- dp[1] = 1
- dp[2] = 2
三种解法
解法一:递归(暴力法)
直接按照状态转移方程递归实现,存在大量重复计算,时间复杂度为 O(2ⁿ)。
解法二:动态规划(数组法)
使用 dp 数组存储中间结果,避免重复计算,时间复杂度为 O(n),空间复杂度为 O(n)。
package cn.wolfcode.demo.servlet;
public class PaLouTi {
public static int pa2(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
解法三:动态规划(空间优化法)
由于 dp[n] 只依赖 dp[n-1] 和 dp[n-2],可以用两个变量代替整个数组,空间复杂度降为 O(1)。
package cn.wolfcode.demo.servlet;
public class PaLouTi {
public static int pa3(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 2;
} else {
int x = 1; // dp[1]
int y = 2; // dp[2]
int z = 0; // dp[n]
for (int i = 3; i <= n; i++) {
z = x + y;
x = y;
y = z;
}
return z;
}
}
}
爬楼梯解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 递归 | O(2ⁿ) | O(n) | 存在大量重复计算,不推荐 |
| DP数组 | O(n) | O(n) | 用数组存储中间结果 |
| DP空间优化 | O(n) | O(1) | 只用两个变量,最优 |
三、01背包问题
问题描述
有一个容量为 V 的背包,还有 n 个物体。每个物体都有两个属性:重量 w 和价值 val。每个物体只有一件,问如何选择装入背包的物品,使得背包中物品的总价值最大?
示例数据
| 物品 | 重量 w | 价值 val |
|---|---|---|
| 物品1 | 1 | 1500 |
| 物品2 | 4 | 3000 |
| 物品3 | 3 | 2000 |
背包容量 V = 4
状态定义
dp[i][j] 表示有 i 个物品可选,放入容量为 j 的背包,产生的最大价值。
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + val[i])
- dp[i-1][j]:不放入第 i 个物品,最大价值等于前 i-1 个物品在容量 j 下的最大价值
- dp[i-1][j-w[i]] + val[i]:放入第 i 个物品,最大价值等于前 i-1 个物品在剩余容量 j-w[i] 下的最大价值加上第 i 个物品的价值
- 取两者中的较大值
解题步骤
- 确认 dp 数组:dp[i][j] 表示前 i 个物品放入容量 j 的背包的最大价值
- 写状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + val[i])
- 写代码:按行优先顺序填充 dp 表
01背包特点
- 每个物品只有一件,要么选要么不选
- 状态转移时只考虑"选"与"不选"两种决策
- 物品个数 n 和背包容量 V 决定了 dp 表的大小为 (n+1) × (V+1)
四、完全背包问题
问题描述
与 01 背包类似,但每个物品有无限件,即物品个数不限制。问如何选择装入背包的物品,使得背包中物品的总价值最大?
状态定义
dp[i][j] 表示有 i 个物品可选(每种物品无限件),放入容量为 j 的背包,最大价值。
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i][j - k*w[i]] + k * val[i])
- dp[i-1][j]:不放入第 i 个物品
- dp[i][j - k*w[i]] + k * val[i]:放入 k 件第 i 个物品(k 从 0 开始尝试)
- 注意这里用的是 dp[i] 而不是 dp[i-1],因为第 i 个物品可以重复选
Java 实现
package cn.wolfcode.demo.servlet;
public class CompleteBackpack {
// N 物品个数 V背包容量
int N, V;
// 重量
private int[] w;
// 价值
private int[] val;
// dp
private int[][] dp;
public void completeBackpack() {
// 数组初始化 省略
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
for (int k = 0; k <= V / w[i - 1]; k++) {
if (j >= k * w[i - 1]) {
dp[i][j] = Math.max(
dp[i - 1][j - k * w[i - 1]] + k * val[i - 1],
dp[i][j]
);
} else {
dp[i][j] = dp[i][j];
}
}
}
}
}
}
完全背包特点
- 每个物品有无限件,可以重复选择
- 状态转移时需要遍历 k(选择该物品的件数)
- 与 01 背包的关键区别:dp[i][j] 的转移来源是 dp[i] 而非 dp[i-1]
五、01背包与完全背包对比
| 对比项 | 01背包 | 完全背包 |
|---|---|---|
| 物品数量 | 每个物品只有一件 | 每个物品有无限件 |
| 状态转移方程 | dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + val[i]) | dp[i][j] = max(dp[i-1][j], dp[i][j-kw[i]] + kval[i]) |
| 转移来源 | dp[i-1](上一行) | dp[i](当前行) |
| 内层循环 | 无需遍历 k | 需要遍历 k(物品件数) |
六、动态规划核心技巧总结
| 技巧 | 说明 |
|---|---|
| 确认 dp 数组含义 | 先明确 dp[i] 或 dp[i][j] 表示什么,这是解题的第一步 |
| 写出状态转移方程 | 找到状态之间的递推关系,这是解题的核心 |
| 确定初始值 | 给边界条件赋值,如 dp[1]、dp[2] |
| 确定遍历顺序 | 一维 dp 注意遍历方向(01背包倒序,完全背包正序) |
| 空间优化 | 如果 dp[i] 只依赖 dp[i-1],可以用一维数组甚至两个变量优化空间 |
七、常见动态规划问题速查
| 问题类型 | 典型题目 | 状态定义 |
|---|---|---|
| 爬楼梯/斐波那契 | 爬楼梯、跳台阶 | dp[n] = dp[n-1] + dp[n-2] |
| 01背包 | 背包问题(物品唯一) | dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + val[i]) |
| 完全背包 | 零钱兑换(无限硬币) | dp[i][j] = max(dp[i-1][j], dp[i][j-kw[i]] + kval[i]) |
| 最长递增子序列 | LIS | dp[i] = max(dp[j] + 1),j < i 且 nums[j] < nums[i] |
| 最长公共子序列 | LCS | dp[i][j] = dp[i-1][j-1]+1 或 max(dp[i-1][j], dp[i][j-1]) |
| 编辑距离 | 字符串编辑 | dp[i][j] 表示 word1 前 i 个字符到 word2 前 j 个字符的最少操作数 |