ACM算法笔记(八)背包问题_01背包

问题描述:

现在给你一个容量为V的背包,有N个物品,其中第i件物品的重量为wi,价值为vi,每件物品只可以拿一次,问在有限的容量内,最多可以拿到多少价值的物品。 

问题分析:

对于每一个物品,都有两种策略:拿或不拿。

读到这里,是不是脑海中有一个清晰的想法?DFS!确实,这不就是我们常见的dfs问题吗,分别枚举拿和不拿两个状态即可。于是写下了如此代码...

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=1e6+7;
int w[MAXN],N,V,v[MAXN],book[MAXN],end=0;
void dfs(int now,int ans,int rest_v)
{
    if(now>N)
    {
        end=max(ans,end);
        return;
    }
    if(book[now]==false&&rest_v>=w[now])
    {
        ans+=v[now];
        rest_v-=w[now];
        book[now]=true;
        dfs(now+1,ans,rest_v);
        ans-=v[now];
        rest_v+=w[now];
        book[now]=false;
    }
    dfs(now+1,ans,rest_v);
}
int main()
{
    cin>>V>>N;
    for(int i=1;i<=N;i++)
    {
        cin>>w[i]>>v[i];
    }
    dfs(1,0,V);
    cout<<end<<endl;
    return 0;
}//标准的暴力dfs模板,遗憾30分qwq

很遗憾对于洛谷P1048的01背包模板题来说,只有30分,剩下70分全部都是TLE(这是什么人间疾苦!)

点此访问洛谷P1048

不过转念一想好像确实,我们平常dfs模板的代码不就是解决小数据问题的么,对于中等数据量和大数据问题,不会吧不会吧不会吧,不会真的有人想写dfs吧?(老阴阳怪气了)

现在我们手动模拟一下...

很显然,当n特别大的时候(我觉得到100之后就已经够呛了)我们的程序变得特别慢,难免TLE(原来是自己老了哈哈哈哈)

不过,车到山前必有路,我们观察这棵横着的递归树,发现是不是有很多重复遍历的部分?

比如选3,选4这个小方案,在选1,选2、选1不选2、不选1选2、不选1不选2的时候都需要访问一次,现在还只是有4个物品的情况,要是有n个物品,那岂不是2^n的数量级了?实在可怕,这样一想,我们之前的代码TLE的关键原因也出来了,就是重复遍历的太多了。

这时候我们就需要一个maxn_数组来辅助剪枝一下,maxn_[i][j]表示前i个物品还剩j点空间下能存放的物品价值的最大值,设想我们现在从不选1选2走到了选3选4这条路上,但是发现还没走之前所选物品的值已经比其他的路到达选3选4的路时所选的物品的值小了,那这条路我们还有必要去走嘛?Ofcourse not!所以就直接return就好咯,这样子就完成了剪枝任务bingo!

如果折现回到代码上,是这样子...

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=1e6+7;
int w[MAXN],N,V,v[MAXN],book[MAXN],end=0,maxn_[1001][1001];
void dfs(int now,int ans,int rest_v)
{
    if(ans<=maxn_[now][rest_v])
        return;
    maxn_[now][rest_v]=ans;
    if(now>N)
    {
        end=max(ans,end);
        return;
    }
    if(book[now]==false&&rest_v>=w[now])
    {
        ans+=v[now];
        rest_v-=w[now];
        book[now]=true;
        dfs(now+1,ans,rest_v);
        ans-=v[now];
        rest_v+=w[now];
        book[now]=false;
    }
    dfs(now+1,ans,rest_v);
}
int main()
{
    cin>>V>>N;
    memset(maxn_,-1,sizeof(maxn_));
    for(int i=1;i<=N;i++)
    {
        cin>>w[i]>>v[i];
    }
    dfs(1,0,V);
    cout<<end<<endl;
    return 0;
}//记忆化搜索AC代码

不过比起递归,01背包最好的方式还是递推,这也是为什么01背包能当作动态规划初步引入例题的原因了。

我们假设dp[i][j]表示前i件物品用j的容量去装,所能达到的最大价值。(是不是很熟悉?没错,就是刚才记忆化搜索用到的那个判断继续不继续的标准)

对于每一次决策,有两个状态:选或不选

对于每一个状态的值,都依赖于上一个状态的值

满足动态规划的条件√

很容易写出状态转移方程:

dp[i][j]=min(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);

说得更清楚一些,dp[i-1][j]不就代表这一件不选,那么就相当于前i-1件物品放入j容量的背包的最大价值,dp[i-1][j-w[i]]代表这一件要了,前i-1件物品放入j-w[i]的容量内的最大价值。

因此我们可以写出以下代码....

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAXN=1e3+7;
int w[MAXN],N,V,v[MAXN],dp[MAXN][MAXN];
int main()
{
    memset(dp,0,sizeof(dp));
    cin>>V>>N;
    for(int i=1;i<=N;i++)
    {
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=N;i++)
    {
        for(int j=V;j>=0;j--)
        {
            if(j>=w[i])
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
            else
                dp[i][j]=dp[i-1][j];
        }
    }
    cout<<dp[N][V]<<endl;
    return 0;
}//二维数组AC代码

但是事情就到这里就结束了吗?当然没有!

没错,还可以接着优化!时间优化?并不,空间优化。毕竟二维数组还是很坑的,万一比赛碰到了一些“善良的死神”,直接MLE了....

从上面我们分析注意到,每一层i的状态都和上一层i-1的状态有关,这样子我们是不是并不需要开二维数组呢?这个时候,滚动数组闪亮出场~

啥是滚动数组?这还要从头忆起,说简单点,滚动数组就是一个很小的数组,一直滚...一直滚...好吧其实就是不断更新的数组。

这里用斐波那契数列来举例子确实再好不过,因为斐波那契数列第i项就是前两项之和,所以对于斐波那契数列来说,有以下代码...

f[1]=1;
f[2]=1;
for(int i=2;i<=n;i++)
{
    f[0]=f[1];
    f[1]=f[2];
    f[2]=f[0]+f[1];
}

这样子最后f[n]就是第n项的值咯~

从滚动数组得到经验,我们用滚动数组来优化我们的二维dp代码...

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAXN=1e3+7;
int w[MAXN],N,V,v[MAXN],dp[100001];
int main()
{
    memset(dp,0,sizeof(dp));
    cin>>V>>N;
    for(int i=1;i<=N;i++)
    {
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=N;i++)
    {
        for(int j=V;j>=0;j--)
        {
            if(j>=w[i])
                dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    cout<<dp[V]<<endl;
    return 0;
}//一维数组AC代码

Ps:对于一维数组,j循环必须要倒序遍历,为了防止多重迭代现象;

举个小例子;

假设给定数据:

10 3

5 5

8 7

4 6

刚开始数组dp内都是初始化为0,因为没有装入任何东西

第一遍遍历,从前往后

第二遍遍历,从前往后

第三次遍历,从前往后

很显然,出现了什么情况呢?居然最大到了17,这里我们可以看到,根据状态转移方程式dp[j]=max(dp[j],dp[j-w[i]]+v[i])来看,在第三次遍历(i=3),还未遍历时dp[8]=7。开始遍历之后,dp[8]=max(dp[8],dp[4]+v[3]),这个时候发现肯定是dp[4]+v[3]更大一些,但是这个时候要注意,dp[4]是已经拿完了第三件物品的,按理来说,我们并没有办法再去拿第三件物品了,因为每一个物品只能拿一件,所以就行不通咯,自然就不可以用正序的方法。而倒序的方法则是先尽可能地都拿着,在最大化的前提下去判断,自然是可以的。

01背包问题就到此完结~完结撒花,感谢陪伴~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值