动规基础——01背包问题(背包问题Ⅱ)

题目来源:领扣 | 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);

  1. 找到递归出口
    上文提到的,背包无剩余空间或者物品算完了,即(j==0||i==0),此时它返回的最大价值应该是0;
  2. 递推关系
    这个关系有两种,比较容易想到的是 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问题有 数字金字塔 等。动态规划太灵活了,没有固定的模板,要根据问题具体分析

    完整代码参考我的GitHub

原文地址:https://www.cnblogs.com/sjh001/p/12041686.html

时间: 2024-10-10 22:49:10

动规基础——01背包问题(背包问题Ⅱ)的相关文章

0-1背包问题(动规基础,好吧虽然我现在在说大话,待续...)

(此位老兄的讲解深得我意,特来推荐:http://blog.csdn.net/insistgogo/article/details/8579597) 有N件物品和一个容量为V的背包.第i件物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大. POINT: 1·每种物品仅有一件,可以选择放或不放. 2·子问题---将前i件物品放入容量为V的背包中.价值总和为f[i][V]; 若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵

POJ3544 Journey with Pigs 动规基础贪心思想

非常经典的贪心题目,没有严格证明的话,肯定是YY着做的,题意: 约翰要从A到B,途中会经过N个村庄,他会带N只猪,然后卖掉,每个村庄卖一只,第i个村庄的人出价pi 每斤,从A到第i个存在的距离为disi,而且运一只猪需要话花费t * disi每斤,t一开始会给定 输入第一行n,t 接下来第一行 n个数,代表各个猪的重量 在接下来第二行n个数,代表每个村庄距离A的距离dis 在接下来第三行n个数,代表每个村庄出价p 输出n个数,表示每个村庄买的哪只猪 这题目一开始看到就往dp方向去想,但是发现不行

动规讲解基础讲解一——01背包(模板)

作为动态规划的基础,01背包的思想在许多动规问题中会经常出现,so,熟练的掌握01背包的思路是极其重要的: 有n件物品,第i件物品(I = 1,2,3…n)的价值是vi, 重量是wi,我们有一个能承重为m的背包,我们选择一些物品放入背包,显然放入背包的总重量不超过m.我们要求选择物品的总价值最大,请问如何选择?这里我们假设所有出现的数都是正整数. 第一想法是? (1) 枚举?万能的枚举啊.但对于n件物品,每件都可以选择取或者不取,总的可能性有2n, n = 30就大约已经有10亿种可能了!枚举所

POJ 3181 Dollar Dayz 01全然背包问题

01全然背包问题. 主要是求有多少种组合.二维dp做的人多了,这里使用一维dp就能够了. 一维的转换方程:dp[j] = dp[j-i] + dp[j];当中i代表重量,j代表当前背包容量. 意思就是dp[j-i] 代表j-i背包重量的时候最多的组合数,那么假设到了背包容量为j的时候,就是能够把第i个物品装进背包,那么就有dp[j-i]种装法, 假设没有i物品之前.那么容量为j的时候组合数是dp[j]. 那么当有i物品,且容量为j的时候,那么组合数就是dp[j-i] + dp[j]; 二维能够转

POJ 3181 Dollar Dayz 01完全背包问题

01完全背包问题. 主要是求有多少种组合.二维dp做的人多了,这里使用一维dp就可以了. 一维的转换方程:dp[j] = dp[j-i] + dp[j];其中i代表重量,j代表当前背包容量. 意思就是dp[j-i] 代表j-i背包重量的时候最多的组合数,那么如果到了背包容量为j的时候,就是可以把第i个物品装进背包,那么就有dp[j-i]种装法, 如果没有i物品之前,那么容量为j的时候组合数是dp[j]: 那么当有i物品,且容量为j的时候,那么组合数就是dp[j-i] + dp[j]; 二维可以转

算法&lt;初级&gt; - 第五章 递归与动规相关问题(完结)

算法<初级> - 第五章 递归与动规相关问题(完结) <一>递归和动态规划 暴力递归 转化为规模缩小了的同问题的子问题 - 时间复杂度O(2n-1) 有明确的边界条件(base case) - 先写base case,再写问题递归的过程 有得到子问题结果后决策过程 不记录每个子问题的解 - 每次求解子问题都交给递归去解决,不会在全局保存子问题的解(与动规形成对比) 动态规划DP 从暴力递归中延申 - 过程中还经历过<记忆化搜索>,相当于暴力递归+cache缓存(用has

HDU 2844 Coins (动规)

Coins Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 6904    Accepted Submission(s): 2809 Problem Description Whuacmers use coins.They have coins of value A1,A2,A3...An Silverland dollar. One

【BZOJ3875】【Ahoi2014】骑士游戏 SPFA处理有后效性动规

广告: #include <stdio.h> int main() { puts("转载请注明出处[vmurder]谢谢"); puts("网址:blog.csdn.net/vmurder/article/details/44040735"); } 题解: 首先一个点可以分裂成多个新点,这样就有了图上动规的基础. 即f[i]表示i点被消灭的最小代价,它可以由分裂出的点们更新. 但是这个东西有后效性,所以我们用SPFA来处理它. spfa处理后效性动规 我

HDU 1171 Big Event in HDU (动规)

Big Event in HDU Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 22623    Accepted Submission(s): 7948 Problem Description Nowadays, we all know that Computer College is the biggest department