背包问题
01背包问题
有 n 个不同种物品,它们有各自的体积和价值,现有给定一个固定容量的背包,如何让背包里装入的物品具有最大的价值总和?
如下即:eg:物品个数为 4,背包总容量为 8
i(物品编号) | 1 | 2 | 3 | 4 |
---|---|---|---|---|
w(体积) | 2 | 3 | 4 | 5 |
v(价值) | 3 | 4 | 5 | 6 |
二维法
定义变量
v
数组:表示每一个物品的价值 w
数组:表示每一个物品的所占用的空间
maxWeight
:表示背包最大容量 nums
表示物品的最大个数
定义 dp(i,j)
二维数组表示:当前背包容量为 j 时,前 i 个物品所能组成的最大价值(其中 j 为动态分配的背包,容量是不断变化的。1<= j <=maxWeight、1<= i <=nums,j 仅仅作为能不能装物品的界限)。
递归关系式
面对当前商品 i 有两种可能性:
- 包的容量比该商品体积小,装不下,此时的价值与前 i-1 个的价值是一样的,即
dp(i,j)=dp(i-1,j)
; - 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即
dp(i,j)=max{dp(i-1,j),dp(i-1,j-w(i))+v(i)}
。
其中dp(i,j)=dp(i-1,j)
表示第 i 个物品不装,dp(i-1,j-w(i))+v(i))
表示装了第 i 个物品,背包容量减少了 w(i),价值增加了 v(i)。
如何理解dp(i-1,j-w(i))
?
当第i个物品被装进背包,肯定占用了 w(i) 的空间,此时背包里面还有 j-w(i) 的空间了,那么在 j-w(i) 的空间里面,还有前 i-1 个物品可以被装进来,则前 i-1 个物品在容量为 j-w(i) 的背包中所能组成的最大价值是多少呢?很明显可以用之前定义的 dp(i,j) 二维数组表示——dp(i-1,j-w(i))
填表
通过填写表即 dp(i,j) 二维数组(当前最大价值表),把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接从二维数组中提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
eg:物品个数为4,背包总容量为8
i(物品编号) | 1 | 2 | 3 | 4 |
---|---|---|---|---|
w(体积) | 2 | 3 | 4 | 5 |
v(价值) | 3 | 4 | 5 | 6 |
定义表空间
初始化边界条件: v(0,j)=v(i,0)=0 即二维数组的第一行和第一列,在给二维数组赋初值的时候就已经做了(这里为了直观的显示就没有将画出来)i:物品编号 j:当前背包容量(动态背包)
i/j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
1 | ||||||||
2 | ||||||||
3 | ||||||||
4 |
填表
从第一行开始填表,当前行
利用前一行
的最优策略
- 如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
- 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
- 如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10
i/j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
1 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
2 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 |
3 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 |
4 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 10 |
一行一行的填完:从第一行到最后一行填完得出最大价值v(4,8)=10
代码实现
/**
* 基础版的01背包(二维法)
* @param n 物品个数
* @param maxWight 背包总容量
* @param wights 每个物品的体积
* @param values 每个物品的价值
*/
public int[][] bag01(int n,int maxWight,int[]wights,int[]values){
int[][] dp=new int[n+1][maxWight+1];
for (int i = 1; i <=n ; i++) {
int w=wights[i-1];
int v=values[i-1];
for (int j = 1; j <=maxWight; j++) {
if (j>=w){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w]+v);
}else {
dp[i][j]=dp[i-1][j];
}
}
}
return dp;
}
一维法
分析二维表
我们会发现二维表有很多重复的,而且我们只用到最后一层的数据,记为i层的数据,要计算第 i 层的数据必须要用到前 i-1 层的数据,只用到这两层,并不会用到其他层的数据。前一层作为基础,进行更新从而变成下一层。一层一层的深入直到最后一层,也就是我们用到的最后一层,那这样就可以用一个一维数组了。
公式对比
直接先从公式对比吧,不然真的不好理解这个公式。
- 二维法:
dp(i,j)=Max(dp(i-1,j),dp(i-1,j-w(i))+v(i))
- 一维法:
dp(j)=Max(dp(j),dp(j-w(i))+v(i))
二维法:直观的将第 i-1 层显示出来,表示前 i-1 个物品在容量为 j-w(i) 的背包中所能组成的最大价值。
一维法:dp(j-w(i))已经隐含了:前 i-1 个物品在容量为 j-w(i) 的背包中所能组成的最大价值。因为随着背包容量的减少w(i),说明第i个物品已经装了,dp (j-w (i) )的值
在前一层已经求出(前提j是逆序的,下面有详细讲解)
for (int j =maxWeight ; j >=1 ; j--) {
if (j>=w){
dp[j]=Math.max(dp[j],dp[j-w]+v);
}
}
逆序问题
新问题出来了———— 你怎么能确定 dp(j-w(i)) 一定是:前 i-1 个物品在容量为 j-w(i) 的背包中所组成的最大价值? 为什么要让 j 逆序的去遍历呢 ?
假设使用正序,则在第 i 层进行更新的过程中, j 从小到大依次递增,当进行到dp(j)=Max(dp(j),dp(j-w(i))+v(i))
时,特别是计算其中的dp(j-w(i))
时必须要用到前 i-1 层的数据。可是我们在第 i 层中在对 j 进行递增( j 正序递增)的过程中,是不是已经将 dp(j-w(i)) 进行更新过了?因为在 j 的递增中会有之前的j
小于现在的j
,现在的j-w(i)
可能正好等于之前的j
,那么此时 dp(j-w(i)) 的数据就不是前 i-1 层的数据了,而是在第 i 层被我们更新的数据! (理解这一点很重要)
因此,让 j 是逆序的,即动态背包是从大到小的。保证了 j-w(i) 的值只能是前 i-1 层的数据,从而让 dp(j-w(i)) 真正的表示为:在背包容量为 j-w(i) 时,前 i-1 个物品所能组成的最大价值。
代码实现
/**
* 进阶版01背包(一维法)
* @param n
* @param maxWeight
* @param values
* @param weight
* @return
*/
static int[] bag02(int n,int maxWeight,int[]values,int[]weight){
int[] dp=new int[maxWeight+1];
for (int i = 1; i <=n ; i++) {
int w=weight[i-1];
int v=values[i-1];
for (int j =maxWeight ; j >=1 ; j--) {
if (j>=w){
dp[j]=Math.max(dp[j],dp[j-w]+v);
}
}
}
return dp;
}
输出结果:[0, 0, 3, 4, 5, 7, 8, 9, 10],恰好为最后一层的结果(第一个元素为边界条件,忽略不计)
背包问题最优解回溯
原理
前面的求解只是求出了背包问题的最优价值,可是并不知道是哪一个物品被装了进来,这该如何是好?
通过填表我们发现
- V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
- V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品i是最优解组成的一部分,记录下来。随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
- 一直遍历到i=1结束为止,所有解的组成都会找到。
代码实现
/**
* 寻找最优解的物品构成
* @param i 为物品个数
* @param j 为背包的总容量
* 其实只需传入 i,j即可,也可以将int[]wights,int[][] dp,int[]item 定义为成员变量
*/
static void findWhat(int i ,int j,int[]wights,int[][] dp,int[]item){
if (i>0) {
int w = wights[i - 1];
if (dp[i][j] == dp[i - 1][j]) {
item[i - 1] = 0;
findWhat(i - 1, j, wights, dp, item);
} else {
item[i - 1] = 1;
findWhat(i - 1, j - w, wights, dp, item);
}
}
}
打印item数组得:[0, 1, 0, 1]即为第二件和第四件物品被装入背包。
完整代码
import java.util.Arrays;
public class bag01 {
public static void main(String[] args) {
int[] wights={2,3,4,5};//每个物品的体积
int[] values={3,4,5,6};//每个物品的价值
int[][] dp = bag01(4, 8, wights, values);
//打印dp数组
for (int i = 1; i <=4 ; i++) {
for (int j = 1; j <=8 ; j++) {
System.out.print(dp[i][j]+" ");
}
System.out.println();
}
int[] item=new int[4];
//寻找最优价值的物品组成
findWhat(4, 8, wights, dp, item);
//打印item数组
System.out.println(Arrays.toString(item));
//一维法求背包问题
int[] dp2= bag02(4, 8, values, wights);
System.out.println(Arrays.toString(dp2));
}
/**
* 基础版的01背包(二维法)
* @param n 物品个数
* @param maxWight 背包总容量
* @param wights 每个物品的体积
* @param values 每个物品的价值
*/
static int[][] bag01(int n,int maxWight,int[]wights,int[]values){
int[][] dp=new int[n+1][maxWight+1];
for (int i = 1; i <=n ; i++) {
int w=wights[i-1];
int v=values[i-1];
for (int j = 1; j <=maxWight; j++) {
if (j>=w){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w]+v);
}else {
dp[i][j]=dp[i-1][j];
}
}
}
return dp;
}
/**
* 进阶版01背包(一维法)
* @param n
* @param maxWeight
* @param values
* @param weight
* @return
*/
static int[] bag02(int n,int maxWeight,int[]values,int[]weight){
int[] dp=new int[maxWeight+1];
for (int i = 1; i <=n ; i++) {
int w=weight[i-1];
int v=values[i-1];
for (int j =maxWeight ; j >=1 ; j--) {
if (j>=w){
dp[j]=Math.max(dp[j],dp[j-w]+v);
}
}
}
return dp;
}
/**
* 寻找最优解的物品构成
* @param i 为物品个数
* @param j 为背包的总容量
* 其实只需传入 i,j即可
*/
static void findWhat(int i ,int j,int[]wights,int[][] dp,int[]item){
if (i>0) {
int w = wights[i - 1];
if (dp[i][j] == dp[i - 1][j]) {
item[i - 1] = 0;
findWhat(i - 1, j, wights, dp, item);
} else {
item[i - 1] = 1;
findWhat(i - 1, j - w, wights, dp, item);
}
}
}
}