01背包问题
01背包问题
一.问题描述
背包问题的基本条件
现有(n + 1)种物品,每种物品只有一个,编号由0到n,每种物品有两个属性,质量weight,价值value;有一个背包,容量(最大承受质量)为capacity;
为了描述每一种物品,我们使用w[n + 1]和v[n + 1]来描述,因此描述第i种物品时,我们使用w[i]表示其质量,v[i]表示其价值
现在往背包中装物品,要求所装入物品的质量和不超过capacity,求装入物品的最大价值
二.解决方案
背包问题是动态规划的经典题型,因为最终的问题的状态可以由前面的状态一步一步推导而来,而且推导过程中存在重复。分析动态规划问题我使用Carl哥的动态规划五部曲。
1.明确dp数组含义
dp[i][j]
表示假设有i + 1种物品(编号0 - i),背包容量为j的情况下,最大的装入物品总价值为dp[i][j]
2.确定递推公式
确定递推公式时,我的思考方式是思考dp[i][j]
如何使用前面的状态(dp[x][y]
)推导而来。我们从dp实际含义的角度出发,发现dp[i][j]
只有可能在两种情况下取:
-
装的物品中没有第i号物品
这种情况下,问题转换为在物品最多考虑到第i - 1号,背包容量为j时,其获取的最大价值。由前面dp数组的含义知,最大价值为
dp[i - 1][j]
-
装的物品中有第i号物品
这种情况下,先算物品最多考虑到第i - 1号(因为每种物品只有一个),背包容量为j - w[i]时(要留出放第i号物品的空间),获取的最大价值为
dp[i - 1][j - w[i]]
,然后再加上第i号物品的价值v[i],所以最大价值为dp[i - 1][j - w[i]] + v[i]
然后我们只要取这两种情况的最大值就可以了,即max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
当然,我们看公式就可以发现这个公式里j - w[i] >= 0,我们结合实际意义发现,j < w[i]意味着背包只放第i号物品都放不下,所以一定是装的物品中没有第i号物品这一情况。
3.确定递推初始化条件
根据公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
,我们可以清楚地看到,i - 1 >= 0,即i >= 1,这样就意味着我们只要确定了i = 0的一行,其他结果就能推出来。根据dp的实际意义,dp[0][j]
代表着在只考虑第0号物品,背包容量为capacity的时候可以获取的最大价值。显然,只要当j >= w[0],背包就可以放下0号物品,dp[0][j]
就可以取得最大价值v[0]。
4.确定递推顺序
根据公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
,可以看到,dp[i][j]
是由左上的数据推出来的,因此,只要保证dp[i][j]
所依赖的左上的数据在算dp[i][j]
时已经推出来了就行,所以只要保证从左到右,从上到下的顺序就行了,先遍历i还是j是无所谓的。
先遍历i再遍历j的代码如下
for(int i = 1; i < nums.lengtd; i++){
for(int j = 0; j <= capicity; j++){
//递推代码
}
}
递推顺序如下所示
1(dp[i - 1][j - w[i]] ) |
2 | 3(dp[i - 1][j] ) |
4 | 5 |
---|---|---|---|---|
6 | 7 | 8(dp[i][j] ) |
... | |
表格中的顺序就是dp[i][j]
被计算出的顺序,i代表行,j代表列
先遍历j再遍历i的代码如下
for(int j = 0; j <= capicity; j++){
for(int i = 1; i < nums.lengtd; i++){
//递推代码
}
}
递推顺序如下所示
1(dp[i - 1][j - w[i]] ) |
5 | 9(dp[i - 1][j] ) |
||
---|---|---|---|---|
2 | 6 | 10(dp[i][j] ) |
||
3 | 7 | ... | ||
4 | 8 |
表格中的顺序就是dp[i][j]
被计算出的顺序,i代表行,j代表列
5.举例推导dp数组
这是为了验证公式的正确性,这里省略了
6.实现代码
根据上面的分析,很容易写出以下实现代码
public static int bag01(int[] weight, int[] value, int capability){
int[][] dp = new int[weight.length][capability + 1];
//初始化
for(int j = 0; j <= capability; j++){
if(weight[0] <= j){
dp[0][j] = value[0];
}
}
//先遍历i,再遍历j,方案一
for(int i = 1; i < weight.length; i++){
for(int j = 0; j <= capability; j++){
if(j < weight[i]){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
//先遍历j,在遍历i,方案二
// for(int j = 0; j <= capability; j++){
// for(int i = 1; i < weight.length; i++){
// if(j < weight[i]){
// dp[i][j] = dp[i - 1][j];
// }else{
// dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
// }
// }
// }
return dp[weight.length - 1][capability];
}
三.空间优化
在上面的代码中,最终的代码时间复杂度为O(vn)
,空间复杂度也为O(vn)
,(v为背包容量,n为物品数量),在时间复杂度方面,已经很难再优化了,但在空间复杂度方面还有优化的空间。
1.思路
再看看递推关系式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
,我们真正想要的,是dp[w.length - 1][capacity]
这个最后一行的最后一个数。我们想优化空间复杂度,就想到能不能去掉i这个维度,通过控制遍历顺序,达到和二维数组相同的效果呢?经过尝试,我们直接把i的维度去掉,递推关系式变为了dp[j] = min(dp[j], dp[j - w[i]] + v[i])
,然后我们想着来控制遍历的顺序来模拟二维数组的数值变化过程。
2.递推顺序
我们是通过控制遍历的顺序来模拟二维数组的数值变化过程。首先,由于公式中可以看到,第i行是由第i - 1行推出来的,所以,i必定是从小到大遍历的。那么,还剩四种情况:
情况1
先遍历i再遍历j,j顺序遍历,也就是如下代码
for(int i = 1; i < nums.lengtd; i++){
for(int j = 0; j <= capicity; j++){
//递推代码
}
}
递推顺序如下所示
1(dp[i - 1][j - w[i]] ) (需要的dp[j - w[i]]的位置) |
2 | 3(dp[i - 1][j] )(此时dp[j]的位置)(需要dp[j]的位置) |
4 | 5 |
---|---|---|---|---|
6(实际dp[j - w[i]] 的位置) |
7 | 8(dp[i][j] ) (待求的dp[j]位置) |
... | |
可以看出,此时我们需要dp[j - w[i]]
的位置在1来求dp[j]
,但此时dp[j - w[i]]
却在6位置,显然不可能求出正确的结果
情况2
先遍历i再遍历j,j逆序遍历,也就是如下代码
for(int i = 1; i < nums.lengtd; i++){
for(int j = capacity; j >= 0; j--){
//递推代码
}
}
递推顺序如下所示
6(dp[i - 1][j - w[i]] ) (需要/实际的dp[j - w[i]]的位置) |
4 | 3(dp[i - 1][j] )(此时dp[j]的位置)(需要dp[j]的位置) |
2 | 1 |
---|---|---|---|---|
... | 9(dp[i][j] ) (待求的dp[j]位置) |
8 | 7 | |
可以看出,此时我们需要dp[j - w[i]]
的位置在6来求dp[j]
,此时dp[j - w[i]]
正好在该位置;需要dp[j]
的位置和此时的位置也正好相同,所以可以求出正确的dp[j]
的值
情况3
先遍历j再遍历i,j顺序遍历,也就是如下代码
for(int j = 0; j <= capicity; j++){
for(int i = 1; i < nums.lengtd; i++){
//递推代码
}
}
递推顺序如下所示
1(dp[i - 1][j - w[i]] ) (需要的dp[j - w[i]]的位置) |
5 | 9(dp[i - 1][j] )(此时dp[j]的位置)(需要dp[j]的位置) |
||
---|---|---|---|---|
2 | 6 | 10(dp[i][j] ) (待求的dp[j]位置) |
||
3 | 7 | ... | ||
4(实际dp[j - w[i]] 的位置) |
8 |
可以看出,此时我们需要dp[j - w[i]]
的位置在1来求dp[j]
,但此时dp[j - w[i]]
却在4位置,显然不可能求出正确的结果
情况4
先遍历j再遍历i,j逆序遍历,也就是如下代码
for(int j = capacity; j >= 0; j--){
for(int i = 1; i < nums.lengtd; i++){
//递推代码
}
}
递推顺序如下所示
17(dp[i - 1][j - w[i]] ) (需要的dp[j - w[i]]的位置) |
13 | 9(dp[i - 1][j] )(此时dp[j]的位置)(需要dp[j]的位置) |
5 | 1 |
---|---|---|---|---|
(此时的dp[j - w[i]]还是初始值) | 14 | 10(dp[i][j] ) (待求的dp[j]位置) |
6 | 2 |
15 | 11 | 7 | 3 | |
16 | 12 | 8 | 4 |
可以看出,此时我们需要dp[j - w[i]]
的位置在17来求dp[j]
,但此时的dp[j - w[i]]还是初始值,显然不可能求出正确的结果
所以遍历顺序应该如情况2所示
3.初始化
这里的初始化有两种方法
-
方法1
类似二维数组的初始化方式,将
dp[j]
中的每一个数初始化为dp[0][j]
的值,i从1开始遍历,具体代码见后面的实现代码部分 -
方法2
初始化为
"i = -1"
的那一行,定义为任何物品都不装的情况下,容量为j的背包的最大价值,显然最大价值为0,此时i从0开始遍历即可
4.实现代码
-
使用方法1初始化
public static int bag01(int[] weight, int[] value, int capability){ int num = weight.length; //dp[j]表示在capability为j时,所能获得的最大价值 int[] dp = new int[capability + 1]; //方法1初始化 for(int j = weight[0]; j <= capability; j++){ dp[j] = value[0]; } for(int i = 1; i <= num - 1; i++){ for(int j = capability; j >= 0; j--){ if(j < weight[i]){ dp[j] = dp[j]; }else{ dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); } } } return dp[capability]; }
-
使用方法2初始化
public static int bag01(int[] weight, int[] value, int capability){ int num = weight.length; //dp[j]表示在capability为j时,所能获得的最大价值 int[] dp = new int[capability + 1]; //方法2初始化 //方法2初始化全初始化为0不用动 for(int i = 0; i <= num - 1; i++){ for(int j = capability; j >= 0; j--){ if(j < weight[i]){ dp[j] = dp[j]; }else{ dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); } } } return dp[capability]; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】