之前讲过了动态规划的几个例子,分别是动态规划 矩阵连乘问题 和 动态规划 最常公共子序列问题,学习了动态规划的使用,而01背包问题作为动态规划的经典问题,同时对贪心算法也是一个很重要的补充,所以也必须掌握01背包问题的原理和实现。
01背包问题
题目描述:
有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?
分析这个问题,使用动态规划的话,我们首先想得是这个问题能不能分成子问题,然后优化子结构。
ps:动态规划自底向上,所以x轴是从 n-> 1,而y轴是从 1->m
看这个图,紫色部分name表示物品的名字,黄色部分weight表示物品的重量,绿色部分value表示物品的价值,蓝色部分表示承重从1到10的背包,白色部分当前承重的背包所能放入的物品的价值的最大值f[i, j]。
这里首先要说一个01背包状态转换方程:
f[i,j] = Max{ f[i+1,j-Wi]+Vi( j >= Wi ), f[i+1,j] }
- f[i,j]表示在前i件物品中选择若干件放在承重为 j 的背包中,可以取得的最大价值。
- Vi表示第i件物品的价值
这个式子现在不懂没关系,文章下会解释
问题分析
动态规划的要求就是从底向上,最后一行e,e的重量是4,价值为6,因此0-3的背包放不下,4-10的背包放得下,所以0-3背包的价值为0,4-10的背包价值为6。
然后从下往上看第二行,例如d4,表示承重为4的背包所能翻入d,e两个物品的价值的最大值,在这里承重4明显不能放入d,只能放入e,因为d的weight为5,e的weight为4,所以d4的最大值就是放入e时的价值最大值6,以此类推。
再来看a8 = 15,是怎么来的呢?按照公式,求a8则需要求 b6 + Va 和 b8,取其最大值,那么因为动态规划是自底向上的,所以当我们求a哪一行的时候,bcde行都已经求出,因此b6+Va = 15 > b8 = 9,所以a8应该等于15。
这个公式该怎么理解呢?或者说为什么f[i,j] 就一定会等于f[i+1,j-Wi]+Vi 和 f[i+1,j] 的最大值?
我们根据动态规划的思想来思考,使用动态规划,就是因为该问题可以通过分解为子问题的分治思想,求出各个子问题的最优子结构,从而得出我们问题的解。
a8(f[i][j]),表示承重为8的背包放入a-e物品的最大价值,
b6 + Va (f[i+1][j - w[i]] + Vi):表示我有一个承重为6的背包(原承重为8 减去 a的重量2),能放入bcde物品的最大价值,再加上a的重量的最大价值
b8 (f[i+1,j]):表示有一个承重为8的背包,能放入bcde物品的最大价值
这样考虑:我们求a8,用优化子结构的思想,先求预留了a的空间的背包的最大价值,再加上a的价值,不就是我需要求得的背包的最大价值了吗?
放入a之后,背包剩余的空间就是8-2 = 6,那么背包为6时的放入bcde物品的最大价值是9,再加上a的价值6等于15,得出我们承重为8的背包最大价值是15,但是还需要考虑下面这个原则:
在同样的背包承重下,如果放入物品i的背包最大价值小于不放入物品i的背包最大价值,我们选择不放入该物品(因为价值都一样,放入了反而占空间,不放此物品我还可以放其它物品)因此我们还要考虑不放入物品a的情况即b8 (f[i+1,j]),b8 = 9,明显放入a之后价值为15 大于 不放入时的价值9,所以我们选择放入
总结:
由以上分析可得,a8=15
代码实现
#include<iostream>
#include<algorithm>
using namespace std;
const int n = 5;//表示物品的数量
const int m = 10;//表示背包所能承受的重量,从1-10
int weight[n+1] = {0, 2, 2, 6, 5, 4};//物品的重量,前面0下标为0只是为了让下标对齐
int value[n+1] = {0, 6, 3, 5, 4, 6};//物品的价值
int f[n+1][m+1];//表示能承受重量为j的背包放入1-i物品的最大价值
void package01(){
int i = n, j;
//首先对最底下的进行填充
for(j = 1; j <= m; j++){
if(j < weight[i]){
f[i][j] = 0;
}else{
f[i][j] = value[i];
}
}
//然后对剩下的n-1个物品填充
for(i = n -1; i > 0; i--){
for(j = 1; j <= m; j++){
if(j < weight[i]){
f[i][j] = f[i+1][j];
}else{
f[i][j] = max(f[i+1][j-weight[i]] + value[i], f[i+1][j]);
}
}
}
for(i = 1; i <= n; i++){
for(j = 1; j <= m; j++){
cout << f[i][j] << " ";
}
cout << endl;
}
cout << "承重为10的背包最大价值是:" << f[1][10];
}
int main(){
package01();
}
Java版:
public class Package01 {
/**
* 01背包状态转移方程:f[i,j] = Max{ f[i+1,j-Wi]+Vi( j >= Wi ), f[i+1,j] }
* i 表示第几个物品,j表示背包的重量
*/
public void sovle(){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();//表示背包的最大重量
int[] w = new int[n]; //表示每一个物品的重量
int[] v = new int[n]; //表示每一个物品的价值
int[][] f = new int[n][m+1]; //用来表示状态转移方程
for (int i = 0; i < n; i++) {
w[i] = sc.nextInt();
}
for (int i = 0; i < n; i++) {
v[i] = sc.nextInt();
}
//自底向上
//首先构造底部背包
int i = n-1, j = 0;
//首先对最底下的进行填充
for(j = 1; j <= m; j++){
if(j < w[i]){
f[i][j] = 0;
}else{
f[i][j] = v[i];
}
}
//然后对剩下的n-1个物品填充
for(i = n-2; i >= 0; i--){
for(j = 1; j <= m; j++){
if(j < w[i]){
f[i][j] = f[i+1][j];
}else{
f[i][j] = Math.max(f[i+1][j-w[i]] + v[i], f[i+1][j]);
}
}
}
for(i = 0; i < n; i++){
for(j = 1; j <= m; j++){
System.out.print(f[i][j] + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
new Package01().sovle();
}
}