动态规划
说明: 刷题顺序参考《代码随想录》
动态规划:重点题型 —> 背包、打家劫舍、买卖股票问题
1. 背包问题
情景,背包容量V,物品价值w,体积v
如何使得背包的价值最大,这就是背包问题
1.1 0 - 1背包
每件物品只有一件,你可以选择放或者不放, 也就是 0 或者 1
dp[i][j]: 可以放0-i物品,背包重量是j,最大的value;
import java.util.Arrays;
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4; // 背包容量
testWeightBagProblem(weight,value,bagSize);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods + 1][bagSize + 1]; // 给物品增加冗余维,i = 0 表示没有物品可选
// 初始化dp数组,默认全为0即可
// 填充dp数组
for (int i = 1; i <= goods; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i - 1]) { // i - 1 对应物品 i
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i - 1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]); // i - 1 对应物品 i
}
}
}
// 打印dp数组
for(int[] arr : dp){
System.out.println(Arrays.toString(arr));
}
}
}
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); // 数组压缩。为什么数组压缩就一定要使用倒序
// dp[j] 的原理就是复制上一行的,所以,前面的遍历一定不可以改变dp[i] 的值
// 所以一定得倒序
// dp[4],比较的时候会使用到比dp[4]更小的数组,所以dp[0 - 3]就是不能被遍历过
// 因此就需要进行倒序
// 而二维数组就没有这个要求,遍历第二行并不会改变第一行的值
dp[j] : 来自上一行
dp[j - w[i]] + w[i] : 上一行的基础上进行改变
总结:思路和二维是一样的
1.2 完全背包
每件有无限,放几个或者不放
// 递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); // 正序就是完全背包问题了
零钱兑换:dp[j] = dp[j] + dp[j - coins[i]];
0 && 1: dp[i][j] = max(dp[i][j], dp[i - 0][j - 1] + 1);
1.3 总结
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
- 动态规划:494.目标和(opens new window)
- 动态规划:518. 零钱兑换 II(opens new window)
- 动态规划:377.组合总和Ⅳ(opens new window)
- 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
2. 打家劫舍
2.1 打家劫舍1
dp[j] : 当前最大;
dp[j] = Math.max(dp[j - 1], dp[j - 2] + nums[j]);
2.2 打家劫舍2
房屋首尾相接;
比较:0 ~ n -2, 1 ~ n - 1;
2.3 打家劫舍3
树形结构, 父子关系被打劫, 就会报警;
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(){}
TreeNode(int val, TreeNode left, TreeNode right){
this.val = val;
this.left = left;
this.right = right;
}
}
/*
public class LinkNode{
int val;
LinkNode next;
LinkNode(){}
LinkNode(int val) {
this.val = val;
this.next = null;
}
LinkNode(int val, LinkNode next) {
this.val = val;
this.next = next;
}
}
*/
class Solution {
public int rob(TreeNode root) {
int[] res = robAction(root);
return Math.max(res[0], res[1]);
}
int[] robAction(TreeNode root) {
int[] res_root = new int[2]; // 0 - 1
// 0 : 不偷当前结点
// 1 : 偷
if(root == null) return res_root; // {0, 0}
int[] res_left = robAction(root.left);
int[] res_right = robAction(root.right);
res_root[1] = root.val + res_left[0] + res_right[0]; // 偷root, = root.val + 左(0) + 右(0)
res_root[0] = Math.max(res_left[0], res_left[1]) + Math.max(res_right[0], res_right[1]);
// 不偷root, = max(左边) + max(右边)
return res_root;
}
}
3. 买卖股票
3.1 买卖股票1
dp[i][0] 表示第i天持有股票所得最多现金;
dp[i][1] 表示第i天不持有股票的现金;
// 递推公式
dp[0][0] = prices[0];
dp[i][0] = max(dp[i - 1][0], -prices[i]); // 只交易一次,持有之前的 && 换新的
dp[i][1] = max(prices[i] + dp[i - 1][0], dp[i - 1][1]);
3.2 买卖股票2 (无限买卖的情况)
dp[i][0] 持有;
dp[i][1] 不持有;
// 递推公式
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 持有,在之前的基础上持有(也就是dp[i - 1][1])
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); // 卖出
3.3 买卖股票3
// 关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。
/*
第一次持有股票
第一次不持有股票
第二次持有股票
第二次不持有股票
*/
int[][] dp = new int[n][5];
dp[0][1] = -prices[0]; // 第一次持有
dp[0][2] = 0; // 第一次出手
dp[0][3] = -prices[0]; // 第二次持有
dp[0][4] = 0; // 第二次出手
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], prices[i] + dp[i - 1][1]); // 出手基于持有
dp[i][3] = Math.max(dp[i - 1][3], -prices[i] + dp[i - 1][2]); // 出手后再持有
dp[i][4] = Math.max(dp[i - 1][4], prices[i] + dp[i - 1][3]); // 第二次持有的基础上
3.4 买卖股票4
// 1, 3, 4 分别是限制了买卖数量(最多1次,2次,k次 )
// 必须在再次购买前出售掉之前的股票
int[][] dp = new int[n][2 * k + 1]; // 1 - 2 * k
for(int i = 1; i <= k; i ++) {
dp[0][2 * i - 1] = - prices[0]; // 初始化
}
for(int i = 1; i < n; i++) {
for(int j = 1; j <= k; j ++) {
dp[i][2 * j - 1] = Math.max(dp[i - 1][2 * j - 1], dp[i - 1][2 * j - 2] - prices[i]);
dp[i][2 * j] = Math.max(dp[i - 1][2 * j], prices[i] + dp[i - 1][2 * j - 1]);
}
}
return dp[n - 1][2 * k];
3.5 最佳买卖股票时机含冷冻期 ( hard )
int[][] dp = new int[prices.length + 1][2];
dp[1][0] = -prices[0];
/*
dp[i][0] 第i天持有股票收益;
dp[i][1] 第i天不持有股票收益;
情况一:第i天是冷静期,不能以dp[i-1][1]购买股票, 所以以dp[i - 2][1]买股票,没问题
情况二:第i天不是冷静期,理论上应该以dp[i-1][1]购买股票,但是第i天不是冷静期说明,第i-1天没有卖出股票,
则dp[i-1][1]=dp[i-2][1],所以可以用dp[i-2][1]买股票,没问题
*/
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i - 1]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i - 1]);