DP算法应用:背包问题解析与实现

这篇具有很好参考价值的文章主要介绍了DP算法应用:背包问题解析与实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

今天我们来讲一个重要算法就是DP(动态规划)。
首先,在讲这一讲之前,我们再看一看之前讲的贪心法中的硬币问题。我们以硬币问题为例,引入我们今天的主角——动态规划。
前面用贪心法解决的最少硬币问题,要求硬币面值是特殊的。而对于任意面值的硬币问题,就需要用 DP 来彻底解决。

题目描述:有 n 种硬币,面值分别为V1 ,V2 ,⋯,Vn ,数量无限。输入非负整数 S,请你选用硬币,使其和为
S。要求输出最少的硬币组合。

我们先定义一个数组 int cnt[M],其中 cnt[i] 是金额 i 对应的最少硬币数量。如果程序能计算出所有的 cnt[i],0<i<M,那么对输入的某个金额 i,只要查 cnt[i] 就得到了答案。
那么我们该如何计算 cnt[i] 呢? cnt[i] 和cnt[i−1] 又是否有关系呢?这里涉及到一个“递推”的思路,从小问题 cnt[i−1] 的解决递推到大问题cnt[i] 的解决。小问题的解决能推导出大问题的解决。下面我们以 5 种面值 {1,5,10,25,50} 的硬币为例,讲解从 i−1 到 i 的递推过程:
(1) 只使用最小面值的 1 分硬币
初始值cnt[0]=0,其他的 cnt[i] 为无穷大。下面计算cnt[1]。动态规划,背包问题,算法实现只用1分硬币

i=0,cnt[0]=0,表示金额为 0,硬币数量为 0。在这个基础上,加一个 1 分硬币,就前进到金额 i=1,硬币数量 cnt[1] = cnt[0]+1 = cnt[1-1]+1= 1 的情况。同理, i=2 时,相当于在 cnt[1] 的基础上加1个硬币,得到 cnt[2] = cnt[2- 1]+1 = 2。继续这个过程,结果是:动态规划,背包问题,算法实现
我们分析上述过程,可以得到递推关系:cnt[i] = min(cnt[i], cnt[i - 1] + 1);要认真理解这个递推过程,确保完全明白哦。这可是 DP 的重要步骤—“状态转移”。

(2)在使用面值1分硬币的基础上,增加使用第二大面值的5分硬币
此时应该从 cnt[5] 开始,因为比 5 分硬币小的金额,不可能用 5 分硬币实现。动态规划,背包问题,算法实现加上5分硬币

i=5 分时,相当于在 i=0 的基础上加一个 5 分硬币,得到 cnt[5] = cnt[5 - 5] + 1 = 1。而 cnt[5] 也可以是在 i=4 的基础上加上一个 1 ,得到 cnt[5]=5,我们取最小值,得 cnt[5]=1。同理,i=6 分时,有 cnt[6] = cnt[6-5] + 1 = 2,对比原来的 cnt[6]=6,取最小值。继续这个过程,结果是:动态规划,背包问题,算法实现
根据上述过程得到递归关系cnt[i]=min(cnt[i],cnt[i−5]+1)

(3)继续处理其它面值的硬币
在 DP 中,把 cnt[i] 这样的记录子问题最优解的数据称为“状态”;从 cnt[i−1] 或 cnt[i−5] 到 cnt[i] 的递推,称为“状态转移”。用前面子问题的结果,推导后续子问题的解,逻辑清晰、计算高效,这就是 DP 的特点。
这里我本人能力有限,找到一个c++版本的答案,大家看一看理解一下,之后会改成python1版本。

#include<bits/stdc++.h>using namespace std;const int M = 251;                   //定义最大金额const int N = 5;                     //5种硬币int type[N] = {1, 5, 10, 25, 50};    //5种面值int cnt[M];                          //每个金额对应最少的硬币数量const int INF = 0x1FFFFFFF;void solve(){
    for(int k = 0; k< M; k++)        //初始值为无穷大
       cnt[k] = INF;
    cnt[0] = 0;
    for(int j = 0; j < N; j++)
        for(int i = type[j]; i < M; i++)
            cnt[i] = min(cnt[i], cnt[i - type[j]] + 1);    //递推式}int main(){
    int s;
    solve();               //计算出所有金额对应的最少硬币数量。打表
    while(cin >> s){
        if(cnt[s] == INF) cout <<"No answer" << endl;
        else              cout << cnt[s] << endl;
    }
    return 0;}

题目数据读入前就开始调用 solve() 函数,这是用到了“打表”的处理方法。即在输入金额之前,提前用 solve() 算出所有的解,得到 cnt[M] 这个“表”,然后再读取金额 s,查表直接输出结果,查一次表的复杂度只有 O(1)。
这样做的原因是,如果有很多组测试数据,例如 10000 个,那么总复杂度是O(N×M+10000),没有增加多少。如果不打表,每次读一个 s,就用 solve() 算一次,那么总复杂度是 O(N×M×10000),时间几乎多了 1 万倍。
但是现在只打印了最少硬币数量,还没打印出到底需要哪些面值的硬币。
在 DP 中,除求最优解的数量之外,往往还要求输出最优解本身,此时状态表需要适当扩展,以包含更多信息。而在最少硬币问题中,如果要求打印组合方案,需要增加一个记录表 path[i],记录金额 i 需要的最后一个硬币。再利用 path[] 逐步倒推,就能得到所有的硬币。
举个例子:金额 i=6,path[6]=5,表示最后一个硬币是 5 分;然后,path[6-5] = path[1],查path[1]=1,表示接下来的最后一个硬币是 1 分;继续 path[1-1] = 0,不需要硬币了,结束。输出结果:硬币组合是“5分 + 1分”。动态规划,背包问题,算法实现
这个也是c++版本,大家可以看一看理解大概。

#include<bits/stdc++.h>using namespace std;const int M = 251;               //定义最大金额const int N = 5;                 //5种硬币int type[N] = {1,5,10,25,50};    //5种面值int cnt[M];                      //每个金额对应最少的硬币数量int path[M]={0};                 //记录最小硬币的路径const int INF = 0x1FFFFFFF;void solve(){
    for(int k=0; k< M;k++)
        cnt[k] = INF;
    cnt[0]=0;
    for(int j = 0;j < N;j++)
        for(int i = type[j]; i < M; i++)
            if(cnt[i] >  cnt[i - type[j]]+1){
               path[i] = type[j];   //在每个金额上记录路径,即某个硬币的面值
               cnt[i] = cnt[i - type[j]] + 1;  //递推式
            }}void print_ans(int *path, int s) {      //打印硬币组合
    while(path[s]!=0 && s>0){
        cout << path[s] << " ";
        s = s - path[s];
    }}int main() {
    int s;
    solve();
    while(cin >> s){
        if(cnt[s] == INF)
            cout <<"No answer"<<endl;
        else{
            cout << cnt[s] << endl;     //输出最少硬币个数
            print_ans(path,s);             //打印硬币组合
        }
    }
    return 0;}

上面用 DP 解决了硬币问题,看罗勇军老师的博客,罗老师有以下总结:
两个特征:重叠子问题、最优子结构,就可以用 DP 高效率地处理、解决。

1.重叠子问题

首先,子问题是原大问题的小版本,计算步骤完全一样;其次,计算大问题的时候,需要多次重复计算小问题。这就是“重叠子问题”。 一个子问题的多次计算,耗费了大量时间。用 DP 处理重叠子问题,每个子问题只需要计算一次,从而避免了重复计算,这就是 DP 效率高的原因。具体的做法是:首先分析得到最优子结构,然后用递推或者带记忆化搜索的递归进行编程,从而实现了高效的计算。

2.最优子结构

最优子结构的意思是:首先,大问题的最优解包含小问题的最优解;其次,可以通过小问题的最优解推导出大问题的最优解。在硬币问题中,我们把 cnt[i] 的计算,减小为cnt[i−1] 的计算,也就是把原来为i的大问题,减小为i−1的小问题,这是硬币问题的最优子结构。
解析来就是一道DP经典题背包问题
它是 DP 的经典问题。下面通过它来说下有关 DP 的:

状态设计
状态转移方程
两种编码方法
滚动数组

题目描述

小明有一个容量为 V 的背包。

这天他去商场购物,商场一共有 N 件物品,第 i 件物品的体积为 wi,价值为vi。

小明想知道在购买的物品总体积不超过 V 的情况下所能获得的最大价值为多少,请你帮他算算。

输入描述

输入第 1 行包含两个正整数 N,V,表示商场物品的数量和小明的背包容量。

第 2∼N+1 行包含 2 个正整数 w,v,表示物品的体积和价值。

1≤N≤102,1≤V≤103,1≤wi,vi≤103

输出描述

输出一行整数表示小明所能获得的最大价值。

样例输入

5 20
1 6
2 5
3 8
5 15
3 3

样例输出

37

题目分析

我们得先设计好 DP 的状态,才能进行 DP 的转移等
那这题 DP 的状态要如何设计呢?
对于这题,我们可以定义二维数组 dp[][],大小为 N×C(dp[i][j] 表示把前 i 个物品(从第 1 个到第 i个)装入容量为 j 的背包中获得的最大价值)。我们把每个 dp[i][j] 都看成一个背包:背包容量为 j,装 1∼i 这些物品。最后得到的 dp[N][C] 就是问题的答案:把 N 个物品装进容量 C 的背包的最大价值。
假设现在已经递推计算到 dp[i][j] 了,那么我们考虑 2 种情况:
1.第 i 个物品的体积比容量 j 还大,不能装进容量 j 的背包。那么直接继承前i−1 个物品装进容量 j 的背包的情况即可:dp[i][j]=dp[i−1][j]。

2.第 i 个物品的体积比容量 j 小,能装进背包。此时又可以分为 2 种情况:装或者不装第 i个:

a.装第 i 个。从前 i−1 个物品的情况下推广而来,前i−1 个物品是 dp[i−1][j]。第 i 个物品装进背包后,背包容量减少 c[i],价值增加w[i]。所以有:dp[i][j] = dp[i-1][j-c[i]] + w[i]。
b.不装第 i 个。那么:dp[i][j] = dp[i-1][j]。

我们取两种情况的最大值,得状态转移方程是:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i])。

总结一下上述分析:背包问题的重叠子问题是 dp[i][j],最优子结构是 dp[i][j] 的状态转移方程。而对于算法的复杂度:算法需要计算二维矩阵 dp[][],二维矩阵的大小是O(NC) 的,每一项计算时间是 O(1),所以总时间复杂度是O(NC),空间复杂度是 O(NC)。
举个例子详细说明一下:
假设现在有 4 个物品,其体积分别是 {2,3,6,5},价值分别为{6,3,5,4},背包的容量为 9。接下来考虑下填 dp[][] 表的过程,我们按照只装第 1 个物品、只放前 2 个、只放前 3 个…的顺序,一直到装完,这就是从小问题扩展到大问题的过程。表格横向 j,纵向 i,按先横向递增 j,再纵向递增 i 的顺序填表。动态规划,背包问题,算法实现图 dp 矩阵

我们分步来分析:
1.步骤 1:只装第 1 个物品。由于物品 1 的体积是 2,所以背包容量小于 2 的,都放不进去,得
dp[1][0] = dp[1][1] = 0;而当物品 1 的体积等于背包容量时,能装进去,背包价值就等于物品 1 的价值,得 dp[1][2] = 6;而对于容量大于 2 的背包,多余的容量用不到,所以价值和容量 2 的背包一样。
动态规划,背包问题,算法实现图 装第1个物品
2.步骤 2:只装前 2 个物品。如果物品 2 体积比背包容量大,那么不能装物品 2,情况和只装第 1 个一样。见下图中的 dp[2][0] = dp[2][1] = 0,dp[2][2] = 6。接着填 dp[2][3]。物品 2 体积等于背包容量,那么可以装物品 2,也可以不装:
(a)如果装物品 2(体积是 3,价值也是 3),那么可以变成一个更小的问题,即只把物品 1 装到(容量j−3)的背包中。动态规划,背包问题,算法实现图 装第2个物品

(b)如果不装物品 2,那么相当于只把物品 1 装到背包中。
动态规划,背包问题,算法实现图 完成dp矩阵
最后的答案是dp[4][9] :把 4 个物品装到容量为 9 的背包,最大价值是 11。

上面表格中的数字是背包的最大价值,那么如何输出背包方案呢?

要想知道具体装了哪些物品,需要倒过来观察:
dp[4][9]=max(dp[3][4]+4,dp[3][9])=dp[3][9],说明没有装物品 4,用 x4=0 表示;

dp[3][9]=max(dp[2][3]+5,dp[2][9])=dp[2][3]+5=11,说明装了物品 3,x3=1;

dp[2][3]=max(dp[1][0]+3,dp[1][3])=dp[1][3],说明没有装物品 2,x2 =0;

dp[1][3]=max(dp[0][1]+6,dp[0][3])=dp[0][1]+6=6,说明装了物品 1,x1=1。
下图中的实线箭头标识了方案的转移路径。动态规划,背包问题,算法实现

记忆化代码和递推代码

接下来说说动态规划最重要的代码实现部分(有两种实现方法)。处理 DP 中的大问题和小问题,有两种思路:自顶向下(Top-Down,先大问题再小问题)、自下而上(Bottom-Up,先小问题再大问题)。而在编码实现 DP 时,自顶向下用带记忆化搜索的递归编码,自下而上用递推编码。

两种方法的复杂度是一样的,每个子问题都计算一遍,而且只计算一遍。下面我们分别实现自下而上的递推和自上而下的记忆化递归实现。

自下而上
递推代码 - Python

这种“自下而上”的方法,先解决子问题,再递推到大问题。通常填写多维表格来完成,编码时用若干 for循环语句填表。根据表中的结果,逐步计算出大问题的解决方案。这就是上面详解中的递推式的直接实现。

N=3011dp = [[0 for i in range(N)] for j in range(N)]w = [0 for i in range(N)]c = [0 for i in range(N)]def solve(n,C):
    for i in range(1,n+1):
        for j in range (0,C+1):
            if c[i]>j :
                dp[i][j] = dp[i-1][j]
            else :
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i])
    return dp[n][C]   n, C = map(int, input().split())for i in range(1, n+1):
    c[i], w[i] = map(int, input().split())print(solve(n, C))

自上而下
记忆化代码 - Python

N=3011dp = [[0 for i in range(N)] for j in range(N)]w = [0 for i in range(N)]c = [0 for i in range(N)]def solve(n,C):
    for i in range(1,n+1):
        for j in range (0,C+1):
            if c[i]>j :
                dp[i][j] = dp[i-1][j]
            else :
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i])
    return dp[n][C]   n, C = map(int, input().split())for i in range(1, n+1):
    c[i], w[i] = map(int, input().split())print(solve(n, C))

滚动数组

DP 的状态方程常常是二维和二维以上,占用了太多的空间。例如前面代码使用了二维矩阵 int dp[N][C],设 N = 103,C = 104,都不算大。但 int 型有 4 字节,需要的空间是 4×103×104 = 40M,会超过部分竞赛题的空间限制。
不过我们可以用滚动数组大大减少空间。它能把二维状态方程的 O(n2) 空间复杂度,优化到 O(n),更高维的数组也可以优化后减少一维。
这个叫滚动数组的玩意儿是如何优化的?

从状态转移方程 dp[i][j] = max(dp[i-1][j], dp[i-1][j - c[i]] + w[i]) 可以看出,dp[i][] 只和dp[i−1][] 有关,和前面的 dp[i−2][]、dp[i−3][]、⋯ 都没有关系。从前面的图表也可以看出,每一行是从上面一行算出来的,跟更前面的行没有关系。而那些用过的已经无用的 dp[i−2][]、dp[i−3][]、⋯ 多余了,那么干脆就复用这些空间,用新的一行覆盖已经无用的一行(滚动),这样只需要两行就够了。
下面给出滚动数组的两种实现方法,两种实现都很常用:

交替滚动

定义 dp[2][j],用 dp[0][] 和 dp[1][] 交替滚动。这种方法的优点是逻辑清晰、编码不易出错,建议作为初学者的你采用这个方法。
交替滚动 - Python

N = 3011dp = [[0 for i in range(N)] for j in range(2)]    #注意先后w = [0 for i in range(N)]c = [0 for i in range(N)]def solve(n,C):
    now = 0
    old =1
    for i in range(1,n+1):
        old,now = now,old            #交换
        for j in range (0,C+1,1):
            if c[i] > j:
                dp[now][j]=dp[old][j]
            else:
                dp[now][j] = max(dp[old][j], dp[old][j-c[i]]+w[i])
    return dp[now][C]   n, C = map(int, input().split())for i in range(1, n+1):
    c[i], w[i] = map(int, input().split())print(solve(n, C))

自我滚动

用两行交替滚动是很符合逻辑的做法,其实还能继续精简:用一个一维的dp[] 就够了,自己滚动自己。
自我滚动 - Python

N=3011dp = [0 for i in range(N)]w = [0 for i in range(N)]c = [0 for i in range(N)]def solve(n,C):
    for i in range(1,n+1):
        for j in range (C,c[i]-1,-1):
            dp[j] = max(dp[j], dp[j-c[i]]+w[i])
    return dp[C]   n, C = map(int, input().split())for i in range(1, n+1):
    c[i], w[i] = map(int, input().split())print(solve(n, C))

不过滚动数组也有缺点。它覆盖了中间转移状态,只留下了最后的状态,损失了很多信息,无法回溯,导致不能输出具体的方案。不过,竞赛题目一般不要求输出具体方案,因为可能有多种方案,不方便判题。
以上就是动态规划的基本内容了。文章来源地址https://www.toymoban.com/news/detail-400156.html

到了这里,关于DP算法应用:背包问题解析与实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 背包问题算法全解析:动态规划和贪心算法详解

    计算机背包问题是动态规划算法中的经典问题。本文将从理论和实践两个方面深入探讨计算机背包问题,并通过实际案例分析,帮助读者更好地理解和应用该问题。 背包问题是一种经典的优化问题。有的时候我们需要将有一堆不同重量或者体积的物品放入背包,但是背包容量

    2024年02月09日
    浏览(49)
  • 动态规划DP之背包问题3---多重背包问题

    目录 DP分析: 优化:  二进制优化 例题:         01背包是每个物品只有一个,完全背包问题是每个物品有无限个。         那么多重背包问题就是 每个物品有有限个 。 有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解

    2024年03月20日
    浏览(49)
  • 【Java实现】动态规划算法解决01背包问题

    1、问题描述: 一个旅行者有一个最多能装m公斤的背包,现在有n中物品,每件的重量分别是W1、W2、……、Wn,每件物品的价值分别为C1、C2、……、Cn, 需要将物品放入背包中,要怎么样放才能保证背包中物品的总价值最大? 2、动态规划算法的概述 1)动态规划(Dynamic Progra

    2023年04月09日
    浏览(57)
  • AcWing算法学习笔记:动态规划(背包 + 线性dp + 区间dp + 计数dp + 状态压缩dp + 树形dp + 记忆化搜索)

    算法 复杂度 时间复杂度0(nm) 空间复杂度0(nv) 代码 算法 通过滚动数组对01背包朴素版进行空间上的优化 f[i] 与 f[i - 1]轮流交替 若体积从小到大进行遍历,当更新f[i, j]时,f[i - 1, j - vi] 已经在更新f[i, j - vi]时被更新了 因此体积需要从大到小进行遍历,当更新f[i, j]时,f[i - 1,

    2024年02月21日
    浏览(43)
  • 【十七】【动态规划】DP41 【模板】01背包、416. 分割等和子集、494. 目标和,三道题目深度解析

    动态规划就像是解决问题的一种策略,它可以帮助我们更高效地找到问题的解决方案。这个策略的核心思想就是将问题分解为一系列的小问题,并将每个小问题的解保存起来。这样,当我们需要解决原始问题的时候,我们就可以直接利用已经计算好的小问题的解,而不需要重

    2024年02月03日
    浏览(45)
  • java实现0-1背包问题方案(动态规划-贪心算法-回溯-分支定界)

    动态规划算法时间复杂度较低,能够求解较大规模的问题,但空间复杂度较高,不适用于数据量较大的问题。 贪心算法时间复杂度较低,能够求解较大规模的问题,但不能保证求得的解是最优解。 回溯算法能够求解较小规模的问题,但时间复杂度较高,不适用于数据量较大

    2024年02月01日
    浏览(130)
  • 动态规划——0/1背包问题(全网最细+图文解析)

    ✨动态规划——0/1背包问题(全网最细+图文解析) 作者介绍: 🎓作者 :青花瓷✨ 👀作者的Gitee :代码仓库 📌系列文章推荐: ✨1.数据结构与算法—算法篇之动态规划(一) ✨2.【Java刷题特辑第一章】——【点进来花两把游戏的时间学习晚上睡觉都踏实了】 ✨3.【Java刷题特辑第二章

    2024年02月03日
    浏览(47)
  • 算法系列--动态规划--背包问题(1)--01背包介绍

    💕\\\"趁着年轻,做一些比较cool的事情\\\"💕 作者:Lvzi 文章主要内容:算法系列–动态规划–背包问题(1)–01背包介绍 大家好,今天为大家带来的是 算法系列--动态规划--背包问题(1)--01背包介绍 背包问题是动态规划中经典的一类问题,经常在笔试面试中出现,是非常 具有区分度 的题

    2024年04月16日
    浏览(56)
  • 算法系列--动态规划--背包问题(3)--完全背包介绍

    💕\\\"Su7\\\"💕 作者:Lvzi 文章主要内容:算法系列–动态规划–背包问题(3)–完全背包介绍 大家好,今天为大家带来的是 算法系列--动态规划--背包问题(3)--完全背包介绍 链接: 完全背包 可以发现完全背包问题和01背包问题还是特比相似的 分析: 完全背包问题 是 01背包问题 的推广

    2024年04月25日
    浏览(45)
  • 【算法-动态规划】0-1 背包问题

    💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老 导航 檀越剑指大厂系列:全面总

    2024年02月08日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包