动态规划
动态规划简介
每一个动态规划都是从一个网格开始的。
动态规划主要解决的问题是:求最值
主要的核心思想是:穷举
动态规划特点
1.重叠子问题
2.状态转移方程
3.最优子结构
解题的思路:
明确状态
明确选择
明确dp数组/函数的定义
- 线性dp
- 杨辉三角 leetcode 118题
- 杨辉三角II leetcode 119题
- 打家劫舍 leetcode 198题
- 打家劫舍II leetcode 213题
- 背包dp
- 零钱兑换 leetcode 322题
- 零钱兑换II
- 分割等和子集 leetcode 416题
- 一和零 leetcode 474题
- 盈利计划 leetcode 879题
- 序列dp
- 740
- 1143
- 368
- 354
- 动态规划 + 字符串
- 有效的字符串 leetcode 678题
- 正则表达式匹配 leetcode 10 题
- 通配符匹配 leetcode 44 题
- 不同的子序列 leetcode 115题
记忆化搜索
背包DP
01背包问题
问题描述:给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i]。现挑选物品放入背包中,假定背包能承受的最大重量为 V,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
例题描述:
给定 3 件物品,物品的重量为 weight[]={1,3,1},对应的价值为 value[]={15,30,20}。现挑选物品放入背包中,假定背包能承受的最大重量 W 为 4,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
设置一个dp[w] [n]数组,横坐标为背包的容量,纵坐标为物品的个数。
对于编号为 i 的物品:
- 如果选择它,那么,当前背包的最大价值等于” i 号物品的价值“ 加上 ”减去 i 号物品占用的空间后剩余的背包空间所能存放的最大价值“,即dp [i] [k] = value[i] + dp[i-1] [k-weight[i]];
- 如果不选择它,那么,当前背包的价值就等于前 i-1 个物品存放在背包中的最大价值,即 dp[i] [k] = dp[i-1] [k]
dp[i][k] 的结果取两者的较大值,即:
dp[i][k] = max(value[i] + dp[i-1][k-weight[i]], dp[i-1][k])
简单解法
代码实现如下:
// 横坐标是容量,纵坐标是几号物品。
public int maxValue(int[] weight, int[] value, int W) {
int n = weight.length;
if (n == 0) return 0;
int[][] dp = new int[n][W + 1];
// 先初始化第 0 行,也就是尝试把 0 号物品放入容量为 k 的背包中
for (int k = 1; k <= W; k++) {
if (k >= weight[0]) dp[0][k] = value[0];
else dp[0][k] = 0;
}
for (int i = 1; i < n; i++) {
for (int k = 1; k <= W; k++) {
// 存放 i 号物品(前提是放得下这件物品)
int valueWith_i = (k-weight[i] >= 0) ? (value[i] + dp[i-1][k-weight[i]]) : 0;
// 不存放 i 号物品
int valueWithout_i = dp[i-1][k];
dp[i][k] = Math.max(valueWith_i, valueWithout_i);
}
}
return dp[n-1][W];
}
压缩空间
观察上面的代码,会发现,当更新dp[i] [..]时,只与dp[i-1] [..]有关,也就是说,我们没有必要使用O(n*W)的空间,而是只使用O(W)的空间即可。
public int maxValue(int[] weight, int[] value, int W) {
int n = weight.length;
if (n == 0) return 0;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
//只要确保 k>=weight[i] 即可,而不是 k>=1,从而减少遍历的次数
for (int k = W; k >= weight[i]; k--) {
dp[k] = Math.max(dp[k - weight[i]] + value[i], dp[k]);
}
}
return dp[W];
}
训练试题
「力扣」第 416 题:分割等和子集(中等);
「力扣」第 474 题:一和零(中等);
「力扣」第 494 题:目标和(中等);
「力扣」第 879 题:盈利计划(困难);
完全背包问题
有n种物品,每种物品有无限个,每个物品的重量为weight[i],每个物品的价值为value[i]。现在有一个背包,它所能容纳的重量为total,问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?
为什么不能使用贪心算法进行每次选取最高的价格的?
想要举反例很简单,比如只有两个物品:物品A:价值5,体积5,物品B:价值8:体积7,背包容量为10,物品B的性价比显然要比物品A高,那么用贪心算法必然会选择放入一个物品B,此时,剩余的空间已无法装下A或者B,所以得到的最高价值为8,而实际上,选择放入两个物品A即可得到更高的价值10。所以这里贪心算法并不适用。
状态转移方程
dp[i+1] [j] = Math.max(dp[i+1] [j], dp[i] [j-k * V[i]] + k * P[i]);
横坐标 j 是容量的大小,纵坐标 i 是物品的序号,dp[i] [j] 保存的是当前的最大价值。
private int[][] dp = new int[P.length + 1][T + 1];
public void solve() {
for (int i = 0; i < P.length; i++){
for (int j = 0; j <= T; j++){
for (int k = 0; k * V[i] <= j; k++){
dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]);
}
}
}
System.out.println("最大价值为:" + dp[P.length][T]);
}
}
状态压缩
private int[] newResults = new int[T+1];
private int ksp(int i, int t){
// 开始填表
for (int m = 0; m < i; m++){
for (int n = V[m]; n <= t; n++){
newResults[n] = Math.max(newResults[n] , newResults[n - V[m]] + P[m]);
}
}
return newResults[newResults.length - 1];
}
「力扣」上的 完全背包问题:
- 「力扣」第 322 题:零钱兑换(中等);
- 「力扣」第 518 题:零钱兑换 II(中等);
- 「力扣」第 279 题:
- 「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。
多重背包问题
有n种物品,每种物品有amount[i]个,每个物品的重量为weight[i],每个物品的价值为value[i]。现在有一个背包,它所能容纳的重量为total,问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?