猴子也能看懂的01背包动态规划!超形象详解!小白向!
1. 什么是01背包问题
有一个容量为V的背包,和编号为1…n的n个物品,他们的体积各不相同,价值也各不相同。求问将这些物品放入背包中,如何使背包中的总价值最大。
2. 输入值与输出值
int V 背包容量
int[] c 储存物品体积的数组,c[i]表示第i件物品的体积。
int[] v 储存物品价值的数组,v[i]表示第i件物品的价值。
输出:int maxValue 在这个背包中任意放入1...n这些物品,能够实现的最大总价值
3. 解决思路
首先,创建一个表,用二维数 f[i][j] 组表示
其中,i表示物品编号,j表示背包容量,f[i][j]的含义为:在考虑1到i个物品可以放入,并且背包容量为j的情况下,最优解(即最大背包中物品的总价值)为f[i][j]。这样理解起来比较抽象,我们将这个表格图像化出来方便理解:
对于这个表格单元f[2][4]来说,它就代表了当背包容量为4,可选物品为1-2号物品时,背包总价值的最大值。 所以,当背包容量为V,可选物品为1到n时,我们的最优解是f[n][V]。为了找到这个值,我们将采用递归思路,从f[i][0]和f[0][j]出发,通过一定的递归规则,遍历整个表格并依次推算,最终找到f[n][V]。那么,最关键的便是找到初始条件f[0][0],以及递归条件。
4. 初始条件
显而易见,当f[i][j]中,可选物品和背包容量任何一个值为0时,也就是说没有可选物品,或者背包无法放入东西时,我们的最优解为0。通过遍历将0写入。这是我们的初始条件
5. 递归条件
要寻找递归条件,也就是说我们要寻找表格前项和后项之间的关系,即寻找后项f[i][j]和已知前项f[x][y](x<i,y<j)之间的关系,这样,通过递归,我们就可以从初始条件f[0][j]和f[i][0]出发,通过递归关系寻找f[1][j],f[2][j]…直至f[n][j]。那么,此处的递归条件是什么呢?
-
对于f[i][j],很显然,如果i的体积超过了背包的容量j,那么i肯定不可以被放入背包,此时f[i][j]=f[i-1][j]
-
如果i的体积小于背包容量j,我们可以分为两种情况:
a. 如果我们不将第i件物品放入背包,此时我们分析可以知道,此时的最优解应该和f[i-1][j]相同
b. 如果我们将第i件物品放入背包,我们首先要确保背包有足够空间能够装下第i件物品。在这个条件下,我们为了实现价值最大化,需要背包先在未装入第i件物品的时候实现价值最大化,然后再装入第i件物品
其中, f[i][j]=f[i-1][j-c[i]]+v[i] 可以理解为,此时的最大价值=在背包中腾出物品i所需的空间且没有放入i的情况下的最大价值,加上i的价值。
那么,在求解f[i][j]时,到底放不放入i呢?显而易见,这取决于上面两种情况哪一种算得的最大价值,所以我们得到最终的递归条件为:
f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]},即等于情况a和情况b相比之下较大的那一个最大价值。
这样,我们就将f[i][j]和已经求得的部分关联起来了。最终我们只要从初始条件出发,就能计算出整张表格。
例子
在一个背包容量为10,可用物品为4个(价值和体积见下表)的背包问题中:
求在表格中f[3][6]的值,即可用物品为1…3,背包容量为6时的最大价值
a. 不放入物品3,那么f[3][6]=f[2][6]
b. 放入物品3,那么f[3][6] = f[2][6-c[3]]+v[3] = f[2][6-4]+8 = f[2][2]+8
而最终f[3][6]的取值,只需要取ab中较大的那一个的值就可以了。
5. 最终代码
/**
* @param V 背包的容积
* @param c 储存着1...i物品体积的数组
* @param v 储存着1...i物品价值的数组
* @return 体积为V,可用物品为1...i时,背包的最大价值
*/
public static int maxValue(int V, int[] c, int[] v) {
//初始化所需变量
int n = c.length; //物品数量n
int f[][] = new int[n + 1][V + 1]; //+1是为了满足表格中,背包体积=0和可用物品为0的情况
//初始条件,注意循环条件要覆盖全表格
for (int i = 0; i <= n; i++)
f[i][0] = 0;
for (int j = 0; j <= V; j++)
f[0][j] = 0;
//递归填充表格,注意在获取物品的体积和价值时,使用c[i-1],v[i-1],因为这里的i是指的物品实际标号,其在c[],v[]中的索引位置要-1;
//逐行填充
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= V; j++) {
if (c[i - 1] > j)//如果当前物品i的体积超过了背包容量
f[i][j] = f[i - 1][j];
else
f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - c[i - 1]] + v[i - 1]);
}
}
//返回表格中最后的值
return f[n][V];
}
-----------测试--------------------------------------------------------------------
对于下表的物品,在背包容量=10的时候
输出结果为:
f[][]表格
6. 空间优化!!!
我们很容易发现,其实我们并不需要记录整张表格,因为在递归的时候,我们参考的前值总是左侧和上面一排的!不理解?我们来看上面完成后的f[][]表格
我们前面得到的递归方程为f[i][j] = max{f[i-1][j], f[i-1][j-c[i]]+v[i]}
在max中的两个待选项(即物品i是否放入的两个情况)中,前者是左边一格的数据(蓝色),是本循环上一次得到的数据,后者是上面一排中,相对f[i][j]左侧的数据(橙色),是外循环上一次得到的数据。(黄色为f[i][j])
显然,我们可以通过不断刷新一个一维数组来完成同样的事,如下图:
如果我们从右往左进行多次遍历,不断刷新这个一维数组,我们就可以大大节省空间。
新的递归条件:f[j]=max{f[j], f[j-c[i]]+v[i]}
我们再次进行分析:
- 不放入物品i,此时f[j]就等于上一次循环中f[j]的值(上一次循环中,可选物品自然是0…i-1)
- 放入物品i,此时f[j]等于f[j-c[i]]+v[i](同样,上一次循环可选物品自然是0…i-1)
这样,新的递归条件允许我们使用一维数组完成算法。需要注意的是,我们改变遍历方向的原因,是因为我们需要使用上一次遍历左侧的值,如果我们依旧从左往右遍历,上一次遍历左侧的值将会被覆盖。
7. 优化后代码
import static sun.misc.Version.print;
/**
* @param V 背包的容积
* @param c 储存着1...i物品体积的数组
* @param v 储存着1...i物品价值的数组
* @return 体积为V,可用物品为1...i时,背包的最大价值
*/
public static int maxValue(int V, int[] c, int[] v) {
//+++++++++++++第一步:初始化所需变量+++++++++++++++++
int n = c.length;
//创建动态优化表格f,第一次遍历时可选物品为0, 背包容量为0...V
int[] f = new int[V + 1];
//+++++++++++++第二步:在可选物品为0时,无论背包容量多大,最大价值都是0+++++++++++++++++
for (int i = 0; i <= V; i++)
f[i]=0;
//+++++++++++++第三步:从初始条件开始进行遍历,不断刷新数组以得到最后答案f+++++++++++++++++
for (int i = 0; i < n; i++) {
for (int j = V; j >=0 ; j--) {
if(c[i]>j)
f[j]=f[j];//仅为方便理解,其实这里可以省去
else
f[j]=Math.max(f[j],f[j-c[i]]+v[i]);
}
}
//+++++++++++++第四步:遍历完成,输出结果+++++++++++++++++
return f[V];
}
恭喜!您学会了吗!