DP问题之01背包如此简单

01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi, wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi≤1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
题目介绍:

有 N 件物品和一个容量为 V 的背包,每种物品都有自己的价值w和体积v,并且只能被选择一次,要求在有限的背包容量下,装入的物品总价值最大。

「0-1 背包」是较为简单的动态规划问题,也是其余背包问题的基础。
动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第 i 个物品的做出决策, 「0-1」正好代表不选与选两种决定。

解题思路

版本1 :二维

多种物品,每种物品只有一个.求能获得的最大总价值.

我们考虑是否选择第i件物品时,是需要考虑前i-1件物品对答案的贡献的.

  1. 状态f[i][j]定义:前 i 个物品,背包容量 j 下的最优解(最大价值):

当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0] = 0开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态f[i][j]不断由之前的状态更新而来

  1. 状态转移方程 :f[i][v]=max(f[i−1][v],f[i−1][v−c[i]]+w[i])

    状态转移方程是如何得来的?

对于当前第i件物品,我们需要考虑其是否能让我们得到更优解.显然,根据上面的话

我们选择第i件物品的时候,我们要得到体积为v的背包,我们需要通过填充用i-1件物品填充得到的体积为v-c[i]的背包得到体积为v的背包.

我们需要考虑到v-c[i]的情况.

当不选当前第i件物品的时候,就对应了状态转移方程中的f[i-1][v],

而选择的时候就对应了f[i-1][v-c[i]]+w[i].
得出以下俩种情况 :

(1)当前背包容量不够(j < v[i]),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:

对应代码:f[i][j] = f[i - 1][j]。

(2)当前背包容量够,可以选,因此需要决策选与不选第 i 个物品:

  • 选:f[i][j] = f[i - 1][j - v[i]] + w[i]。
  • 不选:f[i][j] = f[i - 1][j] 。
    我们的决策是如何取到最大价值,因此以上两种情况取 max() 。

例子:

总共:
4件物品 背包容量为5
体积 价值
1         2
2         4
3         4
4         5
则应该选,中间两样东西,体积之和为5,价值之和为8

代码实现
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAXN = 1005;
int v[MAXN];    // 体积
int w[MAXN];    // 价值 
int f[MAXN][MAXN];  // f[i][j], j体积下前i个物品的最大价值 

int main() 
{
    int n, m;   
    cin >> n >> m;
    for(int i = 1; i <= n; i++) 
        cin >> v[i] >> w[i];

    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

    cout << f[n][m] << endl;

    return 0;
}

版本2:一维

考虑一维如何写!

仔细观察会发现,二维状态中,我们的状态每次都会传递给i(就是说我们的前几行会变得没用.)

这就给了我们写一维dp的机会啊
将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。 都是废话,直接往下看重点

(1)状态f[j]定义:N 件物品,背包容量j下的最优解。

(2)注意枚举背包容量j必须从m开始。

(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。
当初这句话看了很久,大家刚开始看的时候是不是有点看不太明白

这句话什么意思呢?
f[j]从f[j - w]转移过来,那么这里的f[j - w]是指f[i - 1][j - w]还是f[i][j - w]就尤为重要
,如果是正序循环,这样的f[j - w]就是f[i][j - w], 因为我们在算f[j]之前,f[j - w]也就是f[i - 1][j - w]就已经算出来了,如果是倒叙循环,就可以完美解决这个问题, 此时的f[j - w]还没计算,是上一层的结果,即f[i - 1][j - w],所以需要倒着写.

还不太清楚,让我们往下看一个例子体会一下

例子一:假设有3件物品,背包的总体积为10
物品       体积      价值
i = 1         4           5
i = 2         5           6
i = 3         6           7
如果 f[0][j] 总共0件物品,所以最大价值为 0, 即 f[0][j] == 0 也是成立的

  如果 j 层循环是递增的: 
  for (int i = 1; i <= n; i++) {
      for (int j = v[i]; j <= m; j++) {
          f[j] = max(f[j], f[j - v[i]] + w[i]);
      }
  }
  当还未进入循环时(初始状态):
  f[0] = 0;  f[1] = 0;  f[2] = 0;  f[3] = 0;  f[4] = 0;  
  f[5] = 0;  f[6] = 0;  f[7] = 0;  f[8] = 0;  f[9] = 0; f[10] = 0;
  当进入循环 i == 1 时:
  f[4] = max(f[4], f[0] + 5); 即max(0, 5) = 5; 即f[4] = 5;
  f[5] = max(f[5], f[1] + 5); 即max(0, 5) = 5; 即f[5] = 5;
  f[6] = max(f[6], f[2] + 5); 即max(0, 5) = 5; 即f[6] = 5;
  f[7] = max(f[7], f[3] + 5); 即max(0, 5) = 5; 即f[7] = 5;
  重点来了!!!
  f[8] = max(f[8], f[4] + 5); 即max(0, 5 + 5) = 10; 即f[8] = 10;
  这里就已经出错了
  因为此时处于 i == 1 这一层,即物品只有一件,不存在单件物品满足价值为10
  所以已经出错了。

如果 j 层循环是逆序的:

  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]);
      }
  }
 当还未进入循环时:
   f[0] = 0;  f[1] = 0;  f[2] = 0;  f[3] = 0;  f[4] = 0;  
   f[5] = 0;  f[6] = 0;  f[7] = 0;  f[8] = 0;  f[9] = 0; f[10] = 0;
   当进入循环 i == 1 时:w[i] = 5; v[i] = 4;
   j = 10:f[10] = max(f[10], f[6] + 5); 即max(0, 5) = 5; 即f[10] = 5;
   j = 9 :f[9] = max(f[9], f[5] + 5); 即max(0, 5) = 5; 即f[9] = 5;
   j = 8 :f[8] = max(f[8], f[4] + 5); 即max(0, 5) = 5; 即f[8] = 5;
   j = 7 :f[7] = max(f[7], f[3] + 5); 即max(0, 5) = 5; 即f[7] = 5;
   j = 6 :f[6] = max(f[6], f[2] + 5); 即max(0, 5) = 5; 即f[6] = 5;
   j = 5 :f[5] = max(f[5], f[1] + 5); 即max(0, 5) = 5; 即f[5] = 5;
   j = 4 :f[6] = max(f[4], f[0] + 5); 即max(0, 5) = 5; 即f[4] = 5;
   当进入循环 i == 2 时:w[i] = 6; v[i] = 5; 
   j = 10:f[10] = max(f[10], f[5] + 6); 即max(5, 11) = 11; 即f[10] = 11;
   j = 9 :f[9] = max(f[9], f[4] + 6); 即max(5, 11) = 5; 即f[9] = 11;
   j = 8 :f[8] = max(f[8], f[3] + 6); 即max(5, 6) = 6; 即f[8] = 6;
   j = 7 :f[7] = max(f[7], f[2] + 6); 即max(5, 6) = 6; 即f[7] = 6;
   j = 6 :f[6] = max(f[6], f[1] + 6); 即max(5, 6) = 6; 即f[6] = 6;
   j = 5 :f[4] = max(f[5], f[0] + 6); 即max(5, 6) = 6; 即f[5] = 6;
   当进入循环 i == 3 时: w[i] = 7; v[i] = 6; 
   j = 10:f[10] = max(f[10], f[4] + 7); 即max(11, 12) = 12; 即f[10] = 12;
   j = 9 :f[9] = max(f[9], f[3] + 6); 即max(11, 6) = 11; 即f[9] = 11;
   j = 8 :f[8] = max(f[8], f[2] + 6); 即max(6, 6) = 6; 即f[8] = 6;
   j = 7 :f[7] = max(f[7], f[1] + 6); 即max(6, 6) = 6; 即f[7] = 6;
   j = 6 :f[6] = max(f[6], f[0] + 6); 即max(6, 6) = 6; 即f[6] = 6;


状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i] 。

我们发现,逆序完美的解决了这个问题,看到这里大家是不是对为什么要逆序有一些认识了

还不清楚?

让我们看第二个例子体会一下:

此时我们手中有一件体积为2,价值为6的物品.

很明显我们可以得到一个填充了2体积,价值为6的背包. (黑色代表体积,红色代表价值)

(中间填充3的过程略过)

此时当我们填充到体积为4的背包的时候,我们发现可以通过填充体积为2的背包得到更大价值.

但是,我们手中只有一件这样的物品,再去填充不就违反了题目规定?

同理,当我们枚举到更大体积的时候,依旧不满足要求.

所以,顺序枚举体积是完全背包做法.(即物品有很多件.

倒叙枚举

其实根据上面应该不难理解,但是既然说到这里,我还是决定写一下.

同样,我们依旧有一个体积为2的物品。

此时倒叙,我们填充背包.(依旧是到4)

我们发现,我们还是可以通过填充体积为2的背包得到体积为4的背包.

不过这个时候,体积为2的背包中并没有物品.

因此,我们填充体积得到的4的背包,只是得到了体积为2的物品的价值.

再度枚举到体积为2的背包时,我们才填充得到体积为2的背包.

而接下来枚举到的物品中,不会再重复使用这一件物品.

即最终的图会是这样(其他部分略过)

比特截图2022-04-28-23-36-20.png

这样我们就能满足题目中的,每个物品只有一件的情况.

而这也是我们01背包倒叙枚举体积的原因.

  1. 状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i] 。
for(int i = 1; i <= n; i++) 
    for(int j = m; j >= 0; j--)
    {
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后,该行自动成立,可省略。
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
            f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后
    }    
    

实际上,只有当枚举的背包容量 >= 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 <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[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]);

    cout << f[m] << endl;

    return 0;
}

如果对小伙伴有帮助的话,不妨点个赞哟!

比特截图2022-04-28-23-48-04.png

  • 77
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值