博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
动态规划——01背包问题
阅读量:6907 次
发布时间:2019-06-27

本文共 2761 字,大约阅读时间需要 9 分钟。

一、最基础的动态规划之一

01背包问题是动态规划中最基础的问题之一,它的解法完美地体现了动态规划的思想和性质。

01背包问题最常见的问题形式是:给定n件物品的体积和价值,将他们尽可能地放入一个体积固定的背包,最大的价值可以是多少。我们可以用费用c和价值v来描述一件物品,再设允许的最大花费为w。只要n稍大,我们就不可能通过搜索来遍查所有组合的可能。运用动态规划的思想,我们把原来的问题拆分为子问题,子问题再进一步拆分直至不可再分(初始值),随后从初始值开始,尽可能地求取每一个子问题的最优解,最终就能求得原问题的解。由于不同的问题可能有相同的子问题,子问题存在大量重叠,我们需要额外的空间来存储已经求得的子问题的最优解。这样,可以大幅度地降低时间复杂度。

有了这样的思想,我们来看01背包问题可以怎样拆分成子问题:

要求解的问题是:在n件物品中最大花费为w能得到的最大价值。显然,对于0 <= i <= n,0 <= j <= w,在前i件物品中最大花费为j能得到的最大价值。

可以使用数组dp[n + 1][w + 1]来存储所有的子问题,dp[i][j]就代表从前i件物品中选出总花费不超过j时的最大价值

可知dp[0][j]值一定为零。那么,该怎么递推求取所有子问题的解呢。显而易见,要考虑在前i件物品中拿取,首先要考虑前i - 1件物品中拿取的最优情况。

当我们从第i - 1件物品递推到第i件时,我们就要考虑这件物品是拿,还是不拿,怎样收益最大。

①:首先,如果j < c[i],那第i件物品是无论如何拿不了的,dp[i][j] = dp[i - 1][j];

②:如果可以拿,那就要考虑拿了之后收益是否更大。拿这件物品需要花费c[i],除去这c[i]的子问题应该是dp[i - 1][j - c[i]],这时,就要比较dp[i - 1][j]和dp[i - 1][j - c[i]] + v[i],得出最优方案。

细节和代码如下:

int n, w;int c[maxn], v[maxn];//c为费用,v为价值int dp[maxn][maxw];int main(){    scanf("%d %d", &w, &n);//w为最大费用,n为数量    for(int i = 1;i <= n;i++)    {        scanf("%d %d", &c[i], &v[i]);//输入,注意这里的下标是从1开始的    }    memset(dp, 0, sizeof(dp));//若不涉及多组输入,这一步其实可以省略    //如果下标从0开始,下面也需要稍作修改    for(int i = 1;i <= n;i++)    {        for(int j = 0;j <= w;j++)        {            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]] + v[i]);//情况②        }    }    printf("%d\n", dp[n][w]);}

这样做,时间复杂度和空间复杂度都是o(nw)

二、空间优化 一维数组实现

在上面的解法中,我们把所有子问题的最优解都用数组保存了下来,实际上,这些最优解并不是一直都有用的。从状态转移方程可以看出,对当前的i,只有i - 1时的最优解才是有用的,再之前的已经不会再被使用了。如果能把已经无用的空间节省出来,空间复杂度能够得到非常大的优化,这在有些问题中是非常必要的。

实际上,只需要一个一维的数组dp[m + 1](省去n那一维)就可以完成任务。外层的循环每一轮开始时,这个数组里存的要么是初始值(i = 0的情况),要么是上一轮递推得到的值(i - 1时的情况),符合原本状态转移方程的要求。

不过,这样做就需要更加的注意递推的方向,更新的顺序。在更新前,数组里存储的是上一轮的值(或初始值),更新后,更新了的位置存储的就是这一轮的值了。在01背包的问题里,我们要从i - 1的情况递推上来,所以要倒着更新。这一点需要着重理解(反了的话就变成完全背包了)。

代码如下:

int c[maxn], v[maxn];//c为费用,v为价值int dp[maxw];//其余部分略for(int i = 1;i <= n;i++){    for(int j = w;j >= c[i];j--)    //这里要反着更新,否则dp[j - c[i]]会比dp[j]先更新,而更新后它对应的就不是i - 1时的状态了    {        dp[j] = max(dp[j], dp[j - c[i]] + v[i]);    }}

三、01背包的常见变形

实践中很难遇到如此标准的01背包模型,大多数情况下,我们都需要根据具体问题对上面的算法做出一定的修改。

这些问题都是背包问题常见的,解决方法也都相同或者相似:

1.求取的不是价值的最大值而是最小值:把max换成min即可,原理相同。

2.费用不一定都是正数:负数没法用作数组的下标,可以考虑把所有的费用都先加上一个较大的数再做处理(平移)。

3.费用存在小数:可以把所用费用都先放大一定的倍数,是所有的费用值都为整数(推荐HDU 1864)。

4.要求恰好装满,即选取的物品的费用之和恰等于最大花费:

相比基础的01背包,这里我们需要一个正常情况不可能出现的值来表示状态非法、不可能实现,这个值可以是-1、-INF之类的,视具体情况而定。

在初始化时,除dp[i][0]的值应为0之外,其他所有值都应为非法值。在状态转移时,首先要判断子问题是否非法。

for(int i = 1;i <= w;i++)//初始化{    dp[i] = -INF;}dp[0] = 0;for(int i = 1;i <= n;i++){    for(int j = w;j >= c[i];j--)    {        if(dp[i - c[i]] != -INF)//判断是否合法,这里其实省去了几种情况,用二维数组实现的话需注意        {            dp[j] = max(dp[j], dp[j - c[i]] + v[i]);        }    }}

 

转载于:https://www.cnblogs.com/sun-of-Ice/p/9431351.html

你可能感兴趣的文章
Git 子模块 - submodule(转)
查看>>
iPhone开发笔记[12/50]:内存泄漏是新手必然要经历的痛,NSMutableArray的正确使用...
查看>>
MySQL常用命令大全
查看>>
查看Android应用签名信息
查看>>
MVC、MVP、MVVM、Angular.js、Knockout.js、Backbone.js、React.js、Ember.js、Avalon.js、Vue.js 概念摘录...
查看>>
WCF学习之旅—WCF服务的批量寄宿(十三)
查看>>
解决“不是有效的win32应用程序”问题
查看>>
分布拟合——正态/拉普拉斯/对数高斯/瑞利 分布
查看>>
WebStorm for Mac(Web 前端开发工具)破解版安装
查看>>
从0开始--倒序输出。
查看>>
吉特仓库管理系统-.NET打印问题总结
查看>>
sqlplus 返回2 由于命令后没有加分号
查看>>
poj 2155 Matrix
查看>>
shell中(),[]和[[]]的区别
查看>>
Centos7.x下Nginx安装及SSL配置与常用命令
查看>>
98. Validate Binary Search Tree
查看>>
【Android】Retrofit 2.0 的使用
查看>>
Java程序员幽默爆笑锦集
查看>>
【勘误】第三章基本变量
查看>>
用友iuap入选2016世界互联网领先科技成果50强
查看>>