01背包问题

背包问题

什么是动态规划?
动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。

0-1背包问题

在这里插入图片描述

如图所所示:动态规划可以使用闫氏dp分析法(分析dp问题可以使用的通用模式),一个问题可以通过两个方面来进行分析:状态表示:f(i,j)(这个题目中是两维,如果遇到更加复杂的题目可能是三维,四维等等),每一维度都表示一种状态(用状态表述更加合适吧).题目中给定的是共有n个物品,体积最多不超过m,所以f(i,j)表示只考虑前i个物品,而总体积不超过j,表示这个状态,当这个状态被更新到f(n,m)的时候就表示得到了结果,那么我们该如何更新这个表达式(进行状态计算)呢?
可以从第i件物品入手,由于一件物品只有取和不取两种方案,所以可以将这个f(i,j)这个集合划分为两种方案:取第i件物品,就是f(i,j),而我们可以通过这样的思想来进行计算,两个数字的大小关系是确定的,比如说1,10必定有1<10,那么我们此时给这两个数字同时加上或同时减去一个相同的数字,会发现这两个数字的大小关系仍然向之前那样不会发生任何变化。因此我们可以使用这种思想,来把第i件物品去掉,相应的,体积也要减少v[i],此时我们就可以计算状态f(i-1,j-v[i])了,然后这个状态计算完成之后,再给这个状态加上w[i](第i件物品的价值),就可以得到f(i,j)了,由此得到状态转移方程:
f(i,j)=f(i-1,j-v[i])+w[i])
然后取出这两种状态中的最大值即为最后的结果max(f(i-1,j),f(i-1,j-v[i])+w[i])

代码:

//朴素写法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N],f[N][N];//f[i][j]表示(i,j)这个状态下的最大值
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&v[i],&w[i]);
    }
    
    //f[0][n]默认是0了,因为没有拿物品,所以自然是0,所以从1开始枚举即可
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            //左边一定存在
            f[i][j]=f[i-1][j];
            //右边不一定存在,当j<v[i]的时候放不下第i个物品
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m];
    return 0;
}

上边的这种写法实际上是二维的,但是可以进行优化一下降成一维的,这样的话会更加快速.
下面介绍为什么会进行这样的优化:
我们由图可以看到,我的每一个f[i][j]所分成的左右两半部分(左边的是f[i-1][j],右边的是f[i-1][j-v[i]]+w[i])
实际上都是用到了上一层i-1的内容的内容,而i-2,i-3等等都没有用到,所以可以考虑使用滚动数组进行优化。
什么是滚动数组:
我的理解是滚动数组就是当我的一个二位数组(或者更高维度的数组)在状态转移时只需要上一层或者上一次的数据,我就可以降维,使得上一层的数据不断被这一层的数据进行覆盖。这样说可能会比较抽象,推荐大家看这位博主的文章,这篇文章的滚动数组讲的很清楚:链接: 滚动数组
因为这个状态转移方程是符合这个条件的,所以可以使用滚动数组进行降维,降维之后,原来的代码:

for(int i=1;i<=n;i++)
{
	for(int j=0;j<=m;j++)
	{	
		f[i][j]=f[i-1][j];
		if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
	}
}

f[i][j]=f[i-1][j];可以直接写成f[j]=f[j];是一个恒等式,删掉
if后的判断条件是只有当j>=v[i]时才生效,因此我们完全可以让j直接从v[i]开始
for(int j=v[i];j<=m;j++)
{
f[j]=max(f[j],f[j-v[i])+w[i]);
}
但是这样的写法是有问题的,因为我们的目的是让max()中的f都是来自上一层的数据,即
f[j-v[i]]对应的应该是f[i-1][j-v[i]];
但是如果直接删掉的话,并且j是从小到大进行枚举的话,我的f[j-v[i]]实际上会在第i曾进行更新,所以得到的应该是f[i][j-v[i]],这样就与原意不符了,为了避免f[j-v[i]]在f[j]之前被更新,将j的枚举顺序逆序,从m开始到v[i]因为在枚举f[j]的时候前边的都没有被枚举过,所以f[j-v[i]]不会在第i层(就是外层循环的i)提前更新,因此f[j-v[i]]就是保留了上一层的f[j-v[i]]即为f[i-1][j-v[i]];

//滚动数组优化:
for(int i=1;i<=n;i++)
{
	for(int j=m;j>=v[i];j++)
	{	
		f[j]=max(f[j],f[j-v[i]]+w[i]);
	}
}
//优化写法完整版:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N],f[N];//f[i][j]表示(i,j)这个状态下的最大值
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&v[i],&w[i]);
    }
    
    //f[0][n]默认是0了,因为没有拿物品,所以自然是0,所以从1开始枚举即可
    for(int i=1;i<=n;i++)
    {
        for(int j=m;j>=v[i];j--)
        {
            //右边不一定存在,当j<v[i]的时候放不下第i个物品
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[m];
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黒猫.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值