动态规划
动态规划的五个要点:
- 确定 dp 数组(dp table)以及下标的含义
- 确定递推公式
- dp 数组如何初始化
- 确定遍历顺序
- 举例推导 dp 数组
01 背包
问题模板
有 n 件物品和一个最多能背重量为 w 的背包, 第 i 件物品的重量是 weight[i]
, 得到的价值是 value[i]
. 每件物品只能用一次, 求解将哪些物品装入背包里物品价值总和最大.
解答
-
设
dp[i][j]
表示从下标为[0-i]的物品里任意取, 放进容量为 j 的背包, 价值总和最大是多少; -
有两个方面:
- 不放当前物品, 则
dp[i][j] = dp[i - 1][j]
; - 放当前物品, 则
dp[i][j] = dp[i - 1][j - weight[i]] + value[i]
.
所以得出,
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
- 不放当前物品, 则
-
初始化:
-
j = 0
时, 背包容量为 0, 所以dp[i][0] = 0
; -
dp[0][j]
, 即: i 为 0, 存放编号 0 的物品的时候, 各个容量的背包所能存放的最大价值. 所以:- 当 $j<weight$ 的时候,
dp[j]
应该是 0, 因为背包容量比编号 0 的物品重量还小; - 当 $j>=weight$ 时,
dp[j]
应该是 value[0], 因为背包容量放足够放编号 0 号物品.
- 当 $j<weight$ 的时候,
for (int j = 0 ; j < weight[0]; j++) { dp[0][j] = 0; } for (int j = weight[0]; j <= bagWeight; j++) { dp[0][j] = value[0]; }
-
-
先遍历背包或者先遍历物品都可以, 这里是先遍历物品
// weight数组的大小 就是物品个数 for(int i = 1; i < weight.size(); i++) { // 遍历物品 for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } }
-
手动模拟测试一下
优化
使用滚动数组将二维数组优化成一维数组
-
在一维 dp 数组中,
dp[j]
表示: 容量为 j 的背包, 所背的物品价值可以最大为dp[j]
-
依旧是两个方面
- 不放当前物品,
dp[j] = dp[j]
- 放当前物品,
dp[j] = dp[j - weight[i]] + value[i]
所以得出:
dp[j]=max(dp[j], dp[j-weight[i]]+value[i])
- 不放当前物品,
-
背包容量为 0 所背的物品的最大价值就是 0, 所以全部初始化为 0 即可
-
遍历顺序, 这里一定要记住倒序遍历
因为要保证遍历时
dp[j - weight[i]]
是上一轮遍历得出的值, 如果正序遍历的话,dp[j - weight[i]]
一定会比dp[j]
先遍历到, 值可能会有改变, 这样就会导致一个物品重复放入背包, 所以要倒序遍历for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量, j 小于 weight[i] 的值一定不会修改, 就没必要遍历了 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } }
-
手动推导测试一下
做题总结
-
需要转化
- 题目中的元素只能用一次;
- 题目会有一个上限, 背包的容量就是这个上限
-
递推公式
- 求一共有多少种情况 494.目标和 , 在递推公式中是需要累加
for i := 0; i < len(nums); i++ { for j := n; j >= nums[i]; j-- { // 如果是求总共有多少情况, 则直接累加 dp[j] += dp[j-nums[i]] } }
- 求类似于最大, 最长的结果474.一和零 , 在递推公式中就是普通的比大小取最大值
for _, c := range cnt01 { for i := m; i >= c[0]; i-- { for j := n; j >= c[1]; j-- { if dp[i-c[0]][j-c[1]]+1 > dp[i][j] { // ! 如果是求最大xxx, 则是比大小然后取最大值 dp[i][j] = dp[i-c[0]][j-c[1]] + 1 } } } }
完全背包
问题模板
有 n 件物品和一个最多能背重量为 w 的背包, 第 i 件物品的重量是 weight[i]
, 得到的价值是 value[i]
. 每件物品只能用无限次, 求解将哪些物品装入背包里物品价值总和最大.
解答
和 01 背包差不多, 不过因为可以无限选取, 所以递推方程变成了 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
优化
用滚动数组优化也是和 01 背包相似, 不过将遍历顺序改为了正序, 因为不用避免重复选取物品的问题
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
做题总结
-
如果求组合数(顺序无关)就是外层
for
循环遍历物品, 内层for
遍历背包; 如果求排列数(顺序有关)就是外层for
遍历背包, 内层for
循环遍历物品求组和: 518.零钱兑换 II
求排列: 377. 组合总和 Ⅳ
个人理解: 求排列需要在每次更新
dp
数组时保证顺序, 所以要把物品的遍历放到内层循环, 求组合顺序无关就可以把物品的遍历放到外层循环 -
可以使用类似于
dp[i][0], dp[i][1]...
表示不同状态, 每个状态由上一个状态推出(有点自动机的感觉), 具体可以看股票一系列的题目 -
定义
dp
数组的时候可以多定义一位, 这样可以避免后续判断负数下标, 例如下面的代码:func maxUncrossedLines(nums1 []int, nums2 []int) int { m, n := len(nums1), len(nums2) // 这里多定义一位 dp := make([][]int, m+1) for i := 0; i <= m; i++ { dp[i] = make([]int, n+1) } ans := 0 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { // 这里使用 i-1 遍历原数组 // dp 数组不用判断下标是否大于 0 if nums1[i-1] == nums2[j-1] { dp[i][j] = dp[i-1][j-1] + 1 } else { dp[i][j] = func(a, b int) int { if a > b { return a } else { return b } }(dp[i-1][j], dp[i][j-1]) } if dp[i][j] > ans { ans = dp[i][j] } } } return ans }
注意: 这样写的话需要分清
dp[i - 1]
和nums[i - 1]
的含义, 前一个i - 1
表示前一个字符, 后者表示当前遍历的字符