01背包问题详解

01背包问题

最近被背包问题难住了,先上题目(背包问题都大同小异,于是我随便找了一个)

有一个背包,可以装得下总重量tt的物品若干,有n个物品,每个物品价值fi,重量ti
问:如何选取物品才能在有限的重量tt中装入价值最大的物品。
如:
input:3 5         //n=3,tt=5;一共有n=3个物品,背包可以容得下tt=5的重量
       1 2         //第一个物品的价值和重量
       5 5         //第二个物品的价值和重量
       4 3         //第三个物品的价值和重量
output:5          //可以容得下价值总和为5

ok,看到这个题目,首先我第一个会想到暴力枚举去解决这个问题,但是那样的话时间复杂度会非常大,大概估计一下,会达到O(2^n),这个是非常恐怖的,我最近在学动态规划,恰巧里面的经典例题有背包问题,时间复杂度会降低到O(n ^2),这个就还很棒了,废话不多说,开始讲一下我的理解。
首先说明一下,什么是动态规划,简称dp(Dynamic Programming),用一句比较富有哲理的话来描述dp就是:“要看谁笑到了最后,暂时的领先说明不了什么问题”。dp还有一个特性就是状态的无后效性,很好理解,就是过去可以影响着现在,但是现在不能改变过去。
dp最重要的就是状态转移方程,所谓状态转移方程,就是过去来影响现在,让过去的结果影响现在,说白了相当于反向递归。
先来说一下用二维数组写这道题(当然不是最优选择,也不是最简单的,但是是最好理解的)。
先定义一个变量n,存放物品总数,然后再定义一个变量tt,存放背包大小(可以容得下的总重量),再然后定义一个数组f[n]存放每个物品的价值和 t[n]存放每个物品的重量 二维数组dp[n][n]。

int f[1005],t[1005];
int dp[1005][1005];
int n,tt;
cin>>n>>tt;
    for(int i=1;i<=n;i++)
        cin>>f[i]>>t[i];

这是铺垫部分,接下来讲最核心的部分,dp数组

for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=tt;j++)
            dp[i][j]=dp[i-1][j];
        for(int j=t[i];j<=tt;j++)
         dp[i][j]=max(dp[i-1][j],dp[i-1][j-t[i]]+f[i]);

    }

我们主要关注上面的代码,dp数组是如何运行的。dp[i][j]表示前i件物品放进总容量为j的背包里能获得的最大的价值为多少,是不是很难理解?那就对了,因为我也被这里困扰了许久。对于一个物品,我们有两种选择,放或是不放进背包里。如果选择不放进背包里的前提是,这个物品太大,背包总容量都不够,或者这个物品就算放进去了,举个例子,比如第三个物品的价值为3,重量为2,第四个物品的价值为2,重量为3,那么我肯定会选择第二个物品对吧,就可以表达成dp[i-1][tt],可以理解成不放第i个物品,前i-1个物品存放在tt的书包里所能获得的最大价值。那么要是放这个物品的话,是不是要在前面放进去的物品中给他留一个位置,就是总容量为tt,给他留位置就是tt-t[i](表示总容量减去第i个物品的重量)然后再求tt-t[i]所能容下物品的最大价值,可以表达成dp[i-1][tt-t[i]]+f[i],是不是很难理解这里?对,这就是精髓部分,来解释一下(你也可以学着自己来解释一下),前i-1个物品(因为第i个物品必放进去,所以我只要知道前i-1个物品放进去的最大价值)放进tt-t[i](为第i个物品留位置)的书包里,然后再加上f[i](第i件物品的价值,既然必放第i件物品,那么肯定要加上第i件物品的价值咯)。那么怎样确定是放还是不放?好办,我直接判断他们两个谁大谁小就好了

max(dp[i-1][tt],dp[i-1][tt-t[i]]+f[i]);

max函数在c++里需要头文件#include < algorithm>,要是c语言就需要创立一个max函数,也不复杂,这里就不多介绍了。
他们谁大我就用哪个方法(放进去还是不放进去),来画一个表格,就用题目的样例来画一个图

tt\n0123
00000
10000
20111
30114
40114
50155

来解释一下这个图,横的表示n,就是前n件物品放进背包里,竖的表示tt,就是当前书包可以容得下的最大重量,表中数字代表前n件物品放进容量为tt的书包中所获得的最大价值,首先不管n=0或者tt=0,都不能放任何东西,所以均为零,这就是它的边界部分,然后我们来看当n=1的时候,就是前n件物品放进tt的书包里所产生的最大价值,第一件物品的重量为2,所以必须tt=2,才能装得下它,从而价值为1,然后当tt等于3的时候,首先需要继承前一个位置的最大价值,因为都是前1个物品,tt=3比tt=2的书包容量还大了,所以肯定能放得下它的价值,这是它的底线,然后进行判断max(dp[i-1][tt],dp[i-1][tt-t[i]]+f[i]);转换一下就是max(dp[0][3],dp[0][1]+f[1]);如果不放第i个物品,总价值就是dp[0][3]=0,要是放就是dp[0][1]+第一个物品的价值1 =1,那么肯定1大啊,所以放进去,那么这个位置就是1,再来举个例子,看一下n=3,tt=3的时候,max(dp[2][3],dp[2][0]+f[3]),就是max(1,0+4)=4,所以选择放进去。讲了这么多,大家应该可以理解了这个状态转移方程了吧。

#include <iostream>
#include<algorithm>
int f[1005],t[1005];
int dp[1005][1005];
using namespace std;
int main()
{
    int n,tt;
    cin>>n>>tt;
    for(int i=1;i<=n;i++)
        cin>>f[i]>>t[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=tt;j++)
            dp[i][j]=dp[i-1][j];
        for(int j=t[i];j<=tt;j++)
         dp[i][j]=max(dp[i-1][j],dp[i-1][j-t[i]]+f[i]);
    }
    cout<<dp[n][tt]<<endl;
    return 0;
}

至于为什么要写dp[i][j]=dp[i-1][j];就是为了保险起见,防止有些题目的测试数据进行跳跃性,所以我们必须写这一步,要不然就不能ac,有点难得解释,举个例子吧,比如输入书包大小50,物品3个,然后第一个物品重量为1,价值100,第二个物品的重量为16,价值为200,第三个物品的重量为35,价值为300,让我们来运行一下,看看输出的结果是什么
运行结果
结果是300,其实我们想一下就知道装入第一个物品和第三个物品,总重量36,不会超过50,总价值是400,也比300大,所以要写这行代码。

dp[i][j]=dp[i-1][j];

这个时间复杂度是n^2,无法进行优化了,但它使用的二维数组,我们可以从空间上降低复杂度,使用一维数组。
其实我前面讲了这么多,其实dp[i][j]的i可以去掉不要了,因为一维数组其实比二维数组少了很多麻烦,第一,它不需要继承前一个的数值,我们直接拿前一个的数值来使用,反正要么修改它,要么就是用它,举个例子,比如当计算到dp[i+1][]时,dp[i-1][]的数值完全用不到,我们只需要用到dp[i][],而用到dp[i][]也是要么等于它的值,要么把他变成一个新值。第二,代码会变得少很多。
所以它的核心的状态转换方程就是(也可以自己推一推)

dp[j] = max(dp[j], dp[j - t[i]] + f[i])

是不是看着舒服了很多,接下来晒出代码。

#include <iostream>
#include<algorithm>
int f[1005],t[1005];
int dp[1005];
using namespace std;
int main()
{
    int n,tt;
    cin>>n>>tt;
    for(int i=1;i<=n;i++)
        cin>>f[i]>>t[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=tt;j>=t[i];j--)
         dp[j]=max(dp[j],dp[j-t[i]]+f[i]);

    }
    cout<<dp[tt]<<endl;
    return 0;
}

对了,有一点要注意,使用一维数组时,需要逆序,比较一下二维数组和一维数组,想想是为什么要这么做,不这么做会怎么样?

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值