题目来源:领扣 | LintCode
有 i 个物品和一个总容量为 j 的背包. 给定数组 weight 表示每个物品的重量和数组 value 表示每个物品的价值,求最大价值。(物品不能分割)
背包问题II
这道题是一道动态规划(dp)算法的基础题,有两种实现方式,分别是递归和递推(迭代),前者比后者好理解。
解题思路
首先,题目的要求是找出最大价值,所以我们要想,怎么存放才能让他的价值最大呢?因为物品具有重量,背包容量也有限,所以我们不能每次都放入最大价值的物品,举个例子,假设背包容量为 10,现在有三个物品对应价值和重量的关系如下表
物品|质量|价值
A 10 8
B 6 5
C 5 4
显然我们就要选B和C,然后有人可能就会灵光一闪,那全都选择性价比(价值/质量)最大的不就行了(贪心算法),但是题目还有要求“物品不能分割”,所以实际上我们不能使用贪心算法。
我们可以从最普通的思考方式得到一个动态规划的递推式子。我们有i个物品,考虑到重量的情况下,按照通常人类的想法就是找出各种组合,然后依次比较这些组合的价值,选最大的那个就行了,所以我们线性遍历i个物品的时候就有两种选择,“要”和“不要”,
每个物品都经历这两个选择,最终就是产生全部的组合方式。当轮到第i个物品的时候,我们的最大价值就是这样一个式子:maxvalue = max(value[i],value[i-1]) (并考虑每次选择背包能不能装得下),意思就是要第i个物品和不要第i个物品那个情况得到的价值最大,因为判断i个物品我们需要知道i-1个时的最大价值,所以这个式子会一直传递到背包满了或者物品都确认完了,所以可以想到用递归来解决这个过程。
递归算法实现
我们需要让程序知道每个物品的信息,因此需要两个数组来储存每个物品对应的信息(也可用一个结构体数组),分别是value[],weight[]。递归程序需要有会变化的参数,物品的价值和重量时不会变的,显然会变的是物品的数量和背包的剩余容量,所以可以写一个函数bag(前i个物品,剩余背包容量j);
- 找到递归出口
上文提到的,背包无剩余空间或者物品算完了,即(j==0||i==0),此时它返回的最大价值应该是0; - 递推关系
这个关系有两种,比较容易想到的是 maxvalue = max(bag(i-1,j),bag(i-1,j-weight[i])+value[i]) 前一个是“不要”,后一个是“要”;
还有一种情况是 当j!=0但是当前选择的物品的重量weight[i]比剩下空间j多了,我们就要跳过这个物品,即强制选择“不要”。
完成这两部我们就能初步得到递归的算法啦!
int bag(int i, int j) //递归实现
{
if (i == 0 || j == 0)
return 0;
if (weight[i] > j)
return bag(i-1,j);
else {
int res = max(bag(i - 1, j), bag(i - 1, j - weight[i]) + value[i]);
return res;
}
//max函数可以自己判断每一种情况下的最大价值(TIPS:对于递归程序不能刻意去思考它的过程,主要理解它的方向)
}
然而这个还不能称为DP,因为这个效率极差,我们发现它计算几百种情况,难免会出现重复的,重复的节点可能是物品为i,重量为j,所以可以建立一个用来记录的二维数组maxdata[i][j],每个i,j都是一种情况,储存这种情况下的最大价值,这样效率就能大大提高
int bag(int i, int j) //递归实现
{
if (maxdata[i][j] != EOF) //一开始让每个点都等于EOF(-1),如果不是-1证明这个情况已经算过了,可以直接return
return maxdata[i][j];
if (i == 0 || j == 0)
return 0;
if (weight[i] > j)
return bag(i-1,j);
else {
int res = max(bag(i - 1, j), bag(i - 1, j - weight[i]) + value[i]);
maxdata[i][j] = res; //没算过就存一下
return res;
}
}
迭代算法
迭代算法和递归算法有一个本质的区别,递归我们是从i个物品一直找到剩下1个,而迭代算法则恰恰相反,要从一个开始找出最大价值,并逐步向上得到i个物品能得到的最大价值,而我们要记录的是前i种物品在各种重量下的最大价值。(这是比较难理解的点)
但是具体的判断方法仍然是“要”和“不要”的问题。因为是从1个开始,所以我们每轮递推的关键点在于重量,递推式还是那个样子max(maxdata[j],max[j-weight[i]]+value[i]);但是我们是根据数组中储存的前一轮的数据进行判断的。
然后再来分析一下这个递推式,假如现在正计算i种物品,maxdata[j]则是i-1种物品对应j重量的最大价值,如果我们“不要”第i个物品,那么最大价值还是i-1个物品对应的最大价值,如果我们“要”第i个物品,那么要对应前i-1个物品在容量扣除weight[i]时的最大价值,然后加上value[i]。这两个数值哪个大就选哪个作为这一轮的最大价值,因为判断完成后上一轮对应的数据就没有用了 所以我们新的数据就可以覆盖在对应的位置,即maxdata[i] = max(maxdata[j],max[j-weight[i]]+value[i]);
核心代码奉上
//递推(迭代) 滚动数组
int f[100] = { 0 }; //f[j]储存前一轮各个重量下最大价值
for (int i = 1; i <= n; i++) { //枚举种类
for (int j = totalweight; j >= 0; j--){ //算前n种的各个重量下最大价值
int next_w = j - weight[i]; //“要”这个物品的情况下对应的下标
if (next_w < 0) next_w = 0; //防止下标越界
if (weight[i] > j)
f[j] = f[j]; //超重则最大价值等于前一轮对应容量下的最大价值,直接“不要”
else
f[j] = max(f[j], f[next_w] + value[i]);
if(i==n) break; //最后一个物品只需要算一个totalweight的就够了
}
}//最终得到的f[totalweight]就是n个物品用整个背包去装能得到的最大价值。
理解这个程序,只要搞懂i = 1 到 i = 2 的过程就能理解全部了。可以随便举个例子:假设背包容量为 12
编号|物品|质量|价值
1 A 10 8
2 B 6 5
3 C 5 4
- 从第1个A开始,我们发现容量大于等于10的时候最大价值就是8,其他的都是0,此时f[]内的数据全是零,于是大于等于10以后的式子都会是f[j] = max(0,0+8),所以就能得到新的f[];
f[] = {0 0 0 0 0 0 0 0 0 0 8 8 8 ……} - 然后在看第二个物品B,
在总容量为12时,有f[12] = max(f[12],f[12-6]+5) f[12]从前一轮得出等于8,f[6] = 0, 所以显然在这个容量的限制下,我们会选择“不要”。
以此类推,我们可以发现直到 5<j<10 我们才会选择"要”,因此可以得到这一轮的f[] = {0 0 0 0 0 0 6 6 6 6 8 8 8……} - 如果还没理解就,继续看下第三个物品C
跟前一轮完全相同的想法,当j=12的时候,f[12] = max(f[12],f[12-5]+4) 我们发现 f[12] = 8 < f[7]+5 = 11; 所以我们选择“要”
因为这是最后一个了所以继续往下算已经没用了,我们已经得到3个物品,12容量下的最大价值则为 11 ;
到这里迭代算法就算讲解完了!!迭代算法的流程图
闲话
第一次写这种博客,感觉还挺有趣的,最重要的是还能让自己复习学过的知识,本篇内容是按自己的理解写的,可能存在逻辑错误或者漏洞,如果发现问题还请评论指教。
动态规划的学习可以参考mooc上郭伟的《算法与程序设计》,最简单的dp问题有 数字金字塔 等。动态规划太灵活了,没有固定的模板,要根据问题具体分析
原文地址:https://www.cnblogs.com/sjh001/p/12041686.html