背包五讲之Ⅰ:0-1背包问题

有关其他的背包问题👇👇👇
背包五讲合集:0-1背包、完全背包、多重背包、分组背包、二维费用的背包。



题目描述

有 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

二维

定义二维数组dp[i][j]表示在只能放前i个物品的情况下,容量为j的背包所能达到的最大总价值。
对应地,数组w[]v[]分别存物品i的重量和价值。

初始状态下有:

  • 枚举所有j使dp[0][j] = 0,表示没有物品可放时的最大价值恒为0
  • 枚举所有i使 dp[i][0] = 0,表示容量为0时的最大价值恒为0

假设已处理好前i-1件物品,对于第i件物品所作出的决策为:

  • 如果j < w[i],背包容不下第i件物品,只能是dp[i][j] = dp[i - 1][j],此时和前i - 1件物品状态相等,表示没放入第i件物品。
  • 如果j >= w[i],背包可以容下第i件物品,此时面临放入还是不放入第i件物品两种选择,要放入的话则是dp[i][j] = dp[i - 1][j - w[i]] + v[i],表示在前i - 1件物品状态基础上,背包失去了w[i]的容量,得到了v[i]的价值。
  • 由于追求价值最大,纠其二者,取一个max()即为最佳决策,所以得到了状态转移方程为

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

枚举所有的物品件数,再嵌套枚举所有的背包容量来更新dp[i][j],最终答案即存在了dp[N][V]里。

C++代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

int N, V;
int w[1010], v[1010];
int dp[1010][1010];

int main(){
    cin >> N >> V;
    for(int i = 1;i <= N;i ++)
        cin >> w[i] >> v[i];;
    for(int i = 0;i <= N;i ++)
        dp[i][0] = dp[0][i] = 0;
    for(int i = 1;i <= N;i ++){
        for(int j = 1;j <= V;j ++){
            dp[i][j] = (j >= w[i]) ? max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]) : dp[i - 1][j];
        }
    }
    cout << dp[N][V] << endl;
    return 0;
}

一维

考虑到定义二维数组时如果数据量较大则可能导致内存溢出等问题,其实该问题可以简化至一维处理。

经过上述分析,不难得出dp[i][j]只与dp[i - 1][j]有关,且更新前者时不会再去更新后者,所以[i]这一维可以不进行存取,而将其体现在外循环上,定义f[j]表示背包容量为j时所能达到的最大总价值,那么第i轮循环的f[j]——>原来的dp[i][j],只受到前一轮循环i - 1时的f[j]影响。

  • 初始状态有:f[0] = 0
  • 状态转移方程变为:

f[j] = max(f[j], f[j - w[i]] + v[i]),注*:右边的f[j]不同于左边的f[j]

接下来有个很细节的点就是内层循环上发生了改变。


此时的 j是从V开始逆序向前循环。原因在于:

使用逆序循环时才能 保证第i轮循环时读取的f[j]i - 1轮循环后更新好的f[j]数据(代表着方程中右边的f[j])。 因为如果是顺序循环j从1到V,中途某处读取到的f[j - w[i]]或者f[j]在前面j较小时已被更新,而此时处在第i轮循环,被更新的部分象征的是原来的dp[i][j](j或j - w[i]均由j表示),但需要比较的却是原来的dp[i - 1][j],于是f[j]数据被 “污染” 了;
所以j从大到小更新对应的f值可以保障读到的f[j较小]是在本轮循环下没被更新的(对应到原来的dp[i - 1][j较小])。

下面以表格形式展现样例中f[j]的更新过程,试着手算一遍会更便于理解:

C++代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

int N, V;
int w[1010], v[1010];
int f[1010];

int main(){
    cin >> N >> V;
    for(int i = 1;i <= N;i ++)
        cin >> w[i] >> v[i];;
    f[0] = 0;
    for(int i = 1;i <= N;i ++){
        for(int j = V;j >= w[i];j --){
            f[j] = max(f[j], f[j - w[i]] + v[i]);
        }
    }
    cout << f[V] << endl;
    return 0;
}

注*: 本来j要遍历V->1的,然后代码应是

    for(int i = 1;i <= N;i ++){
        for(int j = V;j >= 1;j --){
            f[j] = j >= w[i] ? max(f[j], f[j - w[i]] + v[i]) : f[j];
        }
    }

但观察到当j < w[i]之后一直做的是f[j] = f[j]操作,相当于是空操作,所以可以省去j < w[i]的循环次数,将条件j >= 1改为j >= w[i]


分割线:2022.8.18


偶然看到的,原来上面用一维数组来优化的做法是一个已提出的概念,叫滚动数组
具体说明可以参照一下这位博主的文章👉滚动数组(简单说明)
大概含义是讲,对于某些只需要最终答案的题目,我们可以抛弃掉当中一些不必要存贮的数据,来减少空间的使用;做法是维护一个一维数组,先调用其位置之前的旧数据,然后再将当前位置更新覆盖掉原来的旧数据,而在0-1背包中需要逆序遍历才保证了数据更新的有序性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值