前言
本文章对0-1背包问题做整理和总结。
0-1背包
给定一些物品,其重量由数组元素 w[i] 分别表示,物品价值由 v[i] 表示,背包容量 W,其中每种类型物品只有一种(重量和价值均相同),种类数为 w.length 或 v.length,将这些物品装入背包,最终能够得到的物品总价值为多少?
用回溯解决
直接采用回溯方法来做,每一个物品可以选择是否放还是不放,这是一个组合问题,暴力求解参考参考代码如下:
public class Tttest {
int curV=0,ans=0;
public int solution(int[] w,int[] v,int W) { //回溯法来计算背包问题。
solve(w,v,W,0,0);
return ans;
}
public void solve(int[] w,int[] v,int W,int curW,int begin){
for(int i=begin;i<w.length;i++){
if(curW+w[i]>W)
continue;
curW+=w[i];
curV+=v[i];
ans=Math.max(curV,ans);
solve(w,v,W,curW,i+1);
curW-=w[i];
curV-=v[i];
}
}
public static void main(String[] args) {
Tttest t=new Tttest();
int[] weight = new int[] {2, 6, 1, 5, 4};
int[] value = new int[] {5, 9, 2, 10, 11};
int cap = 10;
int ans=t.solution(weight,value,cap);
System.out.println(ans);
}
}
a,0-1背包解决思路
问题分析
如果读者对 0-1 背包的有一些了解,那先忘掉吧。现在呢,将目光放在这个问题本身:每个物品有自身的重量和价值,求将 n 个物品放入大小为 W 的背包中所能获得的最大总价值?
问题也很好理解,不就是放物品嘛,很自然的想到那就一个个往背包里中放入物品(这句话意味着物品放入是有序的),而每一个物品是否能够放入有两个约束条件:能否放入背包内,能否使得总价值最大。
那么一个物品 i 能否放入和之前的物品放入结果有关系,注意:这里说 物品 i 放入背包,意味着 前 i-1 件物品都已经考虑过了是否放入背包。这是有序放入的含义。
如果不能放入的话(或者说不选择放入的话),那么此时能得到的总价值就等于上一件物品放入时得到的总价值(即前 i-1 件物品放入背包所能得到的最大值);如果可以放入的话(或者说选择放入的话),因为在背包中放入 i 会导致背包可用容量的减少,所以总价值为 i 的价值为上一件物品放入容量为 W-w[i] 大小背包的总价值(前 i-1 件物品放入 W-w[i] 容量的背包) + v[i]。然后在放入和不放入时取最大值,便为放入物品 i 时的最大总价值。
那么现在应该能得到此问题的状态描述了,关键字:当前物品 放入某个容量的背包
和 上一件物品放入 某个容量的背包容量
。
最优子结构,一个大问题由一些在描述上基本一样的子问题来解决,在此为:第几个物品,某个具体容量的背包。
重叠子问题,很轻易地能够发现子问题的结果是需要被复用的。
无后效性,在某个特定容量背包放入第 i 个物品所能得到的最大总价值是固定的,在解决该问题的任何时候都不会发生改变。
状态描述
在之前的文章(https://blog.csdn.net/Little_ant_/article/details/124231043)中,对于凑零钱问题的描述为:凑出某个数的答案和凑出一些数 x,y,z…的答案有关,所以凑零钱问题的状态就为凑出某个数 n 的答案,即 f(n)。
0-1背包问题的状态和凑零钱问题的区别是它具有两个影响因素而已,第几个物品,多大的背包。
此问题的状态描述:第 i 个物品放入容量为 j 的背包所能得到的最大总价值
。我们用符号 f(i,j)来表示。注意:刚才分析的时候,往背包放入物品是按序放入的哦!
有时候它也被描述成前 i 个物品放入容量为 j 的背包所能得到的最大总价值。
状态转移
按照刚才的分析过程,写出转移方程为:f(i,j)= max(f(i-1,j),f(i-1,j-w[i])+ v [i])。
a,推导-背包大小
值得一提的是,在刚才的问题分析采用的自顶而下的方式,而后面的推导采用的均是自底而上。
对于具体问题:w = {2, 6, 1, 5, 4},v = {5, 9, 2, 10, 11},W = 10,回顾一下前面的状态定义,我们从背包大小开始做推导:
初始化:当背包大小为 0 时,没有任何一个物品能够装入背包,此时的总价值均为 0 ,所以 dp [i] [0]
也全为 0。
当背包大小为 1 时,
w [0] : 无法放入,此时总价值为 0,dp[0][1]
=0。
w [1] : 无法放入,此时总价值为 0,dp[1][1]
=0。
w [2] : 可以放入:2,不放入:0。总价值最大嘛,所以选择放入,那么此时总价值为 2 ,dp[2][1]
=2。
w [3] : 无法放入:此时总价值为之前放入 w [ 2 ] 时的总价值 2,dp[3][1]
=2。(即为 dp[2][1]
)
w [4] : 无法放入:此时总价值为之前放入 w [ 3 ] 时的总价值 2,dp[4][1]
=2。
那如果背包大小不为 10 而是 1 的话,此时可以得到最终答案为 dp[4][1]=2
。这里的下标 4 表示将所有物品都考虑过了。
当背包大小为 2 时,
w [0] : 可以放入:5,不放入:0。此时总价值为 5,dp[0][2]
=5。
w [1] : 无法放入,此时总价值为之前放入 w [ 0 ] 时的总价值 5,dp[1][2]
=5。
w [2] : 可以放入:需要在背包中腾出大小为 1 的空间,但此时背包已满,余量为 0 ,那么就拿出大小为 2 的 w [ 0 ],装入 w [ 2 ],此时总价值为 2,(= dp[1][1]+v[2]
)不放入:dp[1][2]
=5。所以总价值dp[2][2]
=5。
w [3] : 无法放入:此时总价值为之前放入 w [ 2 ] 时的总价值 5,dp[3][2]
=5。
w [4] : 无法放入:此时总价值为之前放入 w [ 3 ] 时的总价值 5,dp[4][2]
=5。
当背包大小为 3 时,
w [0] : 可以放入:5,不放入:0。此时总价值为 5,dp[0][3]
=5。
w [1] : 无法放入,此时总价值为之前放入 w [ 0 ] 时的总价值 5,dp[1][3]
=5。
w [2] : 可以放入:此时总价值为 5+2=7,(= dp[1][2]+v[2]
)不放入:dp[1][3]
=5。所以总价值dp[2][3]
=7。
w [3] : 无法放入:此时总价值为之前放入 w [ 2 ] 时的总价值 7,dp[3][4]
=7。
w [4] : 无法放入:此时总价值为之前放入 w [ 3 ] 时的总价值 7,dp[4][4]
=7。
…
最后的 dp 数组如下所示:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
1 | 0 | 0 | 5 | 5 | 5 | 5 | 9 | 9 | 14 | 14 | 14 |
2 | 0 | 2 | 5 | 7 | 7 | 7 | 9 | 11 | 14 | 16 | 16 |
3 | 0 | 2 | 5 | 7 | 7 | 10 | 12 | 15 | 17 | 17 | 17 |
4 | 0 | 2 | 5 | 7 | 11 | 13 | 16 | 18 | 18 | 21 | 23 |
最后的 dp[4][10] =23
为最终结果。
代码
将上面思路转为为代码,如下:
public class Tttest {
public int solution1(int[] w,int[] v,int W) {
int num=w.length;
int[][] dp=new int[num][W+1];
for(int j=1;j<=W;j++){//当前背包容量为 j
if(w[0]<=j)//由于第一件物品下标从 0 开始,在此需要做初始化。
dp[0][j]=v[0];
for(int i=1;i<num;i++){ //保存从 [0,i] 件物品放入背包 j 能得到的最大价值
if(w[i]<=j)
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else
dp[i][j]=dp[i-1][j];
}
}
return dp[num-1][W];
}
public static void main(String[] args) {
Tttest t=new Tttest();
//注意此时物品是从下标0开始的。
int[] weight = new int[] {2, 6, 1, 5, 4};
int[] value = new int[] {5, 9, 2, 10, 11};
int cap = 10;
int ans=t.solution1(weight,value,cap); //DP
// int ans=t.solution(weight,value,cap); //回溯
System.out.println(ans);
}
}
b,推导-物品次序
对问题:w = {2, 6, 1, 5, 4},v = {5, 9, 2, 10, 11},W = 10,我们从物品次序开始做推导:
初始化:因为物品下标从 0 开始,所以跳过。但值得一提的是物品下标若从 1 开始,那么 dp[0][j]
均为0,因为此时并没有装入物品。不过所有 dp[i][0]
仍然均为 0,
第一件物品 w[0]:
j = 0:无法放入,此时总价值为 0,dp[0][0]
=0。(即所有 dp[i][0]
均为 0)
j = 1 : 无法放入,此时总价值为 0,dp[0][1]
=0。
j = 2 : 可以放入,此时总价值为 5,dp[0][2]
=5。
j = 3 … 10 : 可以放入,此时总价值为 5,dp[0][j]
=5。
在第一件物品的基础上放入第二件物品 w[1]:
j = 0:无法放入,此时总价值为 dp[0][0]
,所以dp[1][0]
=0。(即所有 dp[i][0]
均为 0)
j = 1:无法放入,此时总价值为 dp[0][1]
(0),所以dp[1][0]
=0。
j = 2 …5: 无法放入,此时总价值为 dp[0][j]
(5)。所以dp[1][j]
=5。
j = 6 : 可以放入:放入的话需要腾出大小为 w[1] 的空间,要把 w[0] 拿出来,然后放入 w[1],此时总价值为 9(也就是 dp[0][0] + v[1]
的值);不放入:dp[0][6]
=5。取最大,所以dp[1][6]
=9。
j = 7 : 可以放入:放入的话需要腾出大小为 w[1] 的空间,要把 w[0] 拿出来,然后放入 w[1],此时总价值为 9(也就是 dp[0][1] + v[1]
的值);不放入:dp[0][7]
=5。取最大,所以dp[1][7]
=9。
j = 8 : 可以放入:放入的话需要腾出大小为 w[1] 的空间,此时空间足够,直接放入 w[1],此时总价值为 14(也就是 dp[0][2] + v[1]
的值);不放入:dp[0][8]
=5。取最大,所以dp[1][8]
=14。
j = 9 . 10:空间足够,直接放入。dp[1][j]
=14。
在前两件物品的基础上放入第三件物品 w[2]:
j = 0:无法放入,此时总价值为 dp[1][0]
,所以dp[2][0]
=0。(即所有 dp[i][0]
均为 0)
j = 1:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时背包里没有东西,仅放入 w[2],总价值为 2(也就是 dp[1][0] + v[2]
的值);不放入:dp[1][1]
=0。取最大,所以dp[2][1]
=0。
j = 2:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时需要拿出 w[0],然后放入,总价值为 2(也就是 dp[1][1] + v[2]
的值);不放入:dp[1][2]
=5。取最大,所以dp[2][2]
=5。
j = 3:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时空间足够,直接放入,总价值为 7(也就是 dp[1][2] + v[2]
的值);不放入:dp[1][3]
=5。取最大,所以dp[2][3]
=7。
j = 4 . 5:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时空间足够,直接放入,总价值为 7(也就是 dp[1][j-1] + v[2]
的值);不放入:dp[1][j]
=5。取最大,所以dp[2][j]
=7。
j = 6:可以放入:放入的话需要腾出大小为 w[2] 的空间,剩下的空间只能放入 w[0],然后放入 w[2],总价值为 7(也就是 dp[1][5] + v[2]
的值);不放入:dp[1][6]
=9。取最大,所以dp[2][6]
=9。
j = 7:可以放入:放入的话需要腾出大小为 w[2] 的空间,剩下的空间可以放入 w[1],然后放入 w[2],总价值为 11(也就是 dp[1][6] + v[2]
的值);不放入:dp[1][7]
=9。取最大,所以dp[2][7]
=11。
j = 8:可以放入:放入的话需要腾出大小为 w[2] 的空间,dp[1][7] + v[2]
=11;不放入:dp[1][8]
=14。取最大,所以dp[2][8]
=14。
j = 9:可以放入:放入的话需要腾出大小为 w[2] 的空间,dp[1][8] + v[2]
=16(能够看出当前容量能够同时放入 w[0],w[1],w[2]);不放入:dp[1][9]
=14。取最大,所以dp[2][9]
=16。
j = 10:可以放入:放入的话需要腾出大小为 w[2] 的空间,dp[1][9] + v[2]
=16;不放入:dp[1][10]
=14。取最大,所以dp[2][10]
=16。
在前三件物品的基础上放入第四件物品 w[3]:
j = 0:无法放入,此时总价值为 dp[2][0]
,所以dp[3][0]
=0。(即所有 dp[i][0]
均为 0)
j = 1:无法放入,而 dp[2][1]
=2。所以dp[3][1]
=0。
j = 2:无法放入,而 dp[2][2]
=5。所以dp[3][2]
=5。
j = 3:无法放入,而 dp[2][3]
=7。所以dp[3][3]
=7。
j = 4:无法放入,而 dp[2][4]
=7。所以dp[3][4]
=7。
j = 5:可以放入,放入的话需要腾出大小为 w[3] 的空间,此时背包里没有东西,仅放入 w[3],总价值为 10(也就是 dp[2][0] + v[3]
的值);不放入:dp[2][5]
=7。取最大,所以dp[3][5]
=10。
j = 6:可以放入,放入的话需要腾出大小为 w[3] 的空间,此时背包余量仅可放入 w[2],放入 w[3],总价值为 12(也就是 dp[2][1] + v[3]
的值);不放入:dp[2][6]
=9。取最大,所以dp[3][6]
=12。
j = 7:可以放入:dp[2][2] + v[3]
=15;不放入:dp[2][7]
=11。取最大,所以dp[3][7]
=17。
j = 8:可以放入:dp[2][3] + v[3]
=17;不放入:dp[2][8]
=14。取最大,所以dp[3][8]
=17。
j = 9:可以放入:dp[2][4] + v[3]
=17;不放入:dp[2][9]
=16。取最大,所以dp[3][9]
=17。
j = 10:可以放入:dp[2][5] + v[3]
=17;不放入:dp[2][10]
=16。取最大,所以dp[3][10]
=17。
在前四件物品的基础上放入第五件物品 w[4],读者可自行推导。
代码
将上面思路转为为代码,参考代码如下:
public void solution1(int[] w,int[] v,int W) {//打印出dp数组
int num=w.length;
int[][] dp=new int[num][W+1];
for (int j=1;j<=W;j++)//对第一件物品 w[0] 单独做初始化
if(w[0]<=j)
dp[0][j]=v[0];
for(int i=1;i<num;i++){
for(int j=1;j<=W;j++){// j=0时,全为0.
if(j>=w[i])
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else
dp[i][j]=dp[i-1][j];
}
}
for(int i=0;i<num;i++){
for(int j=0;j<=W;j++)
System.out.printf("%2d ",dp[i][j]);
System.out.println();
}
}
public static void main(String[] args) {
Tttest t=new Tttest();
//注意此时物品是从下标0开始的。
int[] weight = new int[] {2, 6, 1, 5, 4};
int[] value = new int[] {5, 9, 2, 10, 11};
int cap = 10;
t.solution1(weight,value,cap);
}
最终输出的 dp 数组和从背包大小开始做推导时一模一样,区别在于背包大小是一列一列进行推导,物品次序是一行一行推导,代码都是自底而上,pull 类型的算法实现。
c,物品次序从1开始
对于上面的示例,如果物品次序从 1 开始,那么在代码中就不需要单独处理第一件物品 w[0] 了。以物品次序为例,算法如下:
代码
public class Tttest {
public void solution1(int[] w,int[] v,int W) {//打印出dp数组
int num=w.length;
int[][] dp=new int[num][W+1];
for(int i=1;i<num;i++){
for(int j=1;j<=W;j++){
if(j>=w[i])
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else
dp[i][j]=dp[i-1][j];
}
}
for(int i=0;i<num;i++){
for(int j=0;j<=W;j++)
System.out.printf("%2d ",dp[i][j]);
System.out.println();
}
}
public static void main(String[] args) {
Tttest t=new Tttest();
//注意此时物品是从下标1开始的。
int[] weight = new int[] {0,2, 6, 1, 5, 4};
int[] value = new int[] {0,5, 9, 2, 10, 11};
int cap = 10;
t.solution1(weight,value,cap);
}
}
此时的 dp 数组如下所示:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
2 | 0 | 0 | 5 | 5 | 5 | 5 | 9 | 9 | 14 | 14 | 14 |
3 | 0 | 2 | 5 | 7 | 7 | 7 | 9 | 11 | 14 | 16 | 16 |
4 | 0 | 2 | 5 | 7 | 7 | 10 | 12 | 15 | 17 | 17 | 17 |
5 | 0 | 2 | 5 | 7 | 11 | 13 | 16 | 18 | 18 | 21 | 23 |
第一行表示当没有物品放入任意大小的背包时的总价值,为0。如果物品次序从 0 开始,那么这里数组下标就得取 -1,而数组又不能越界,所以才会对 w[0] 进行额外的处理。
第一列表示将前 i 个物品放入大小为 0 的背包时的总价值,为0。
总结
暂时结束。行文仓促,或有纰漏,欢迎指正!