【背包问题】之01背包和完全背包

2023-10-26

1 01 背包

1.1 题目描述

N N N 件物品和一个容量为 V V V 的背包。放入第 i i i 件物品的体积为 v i v_i vi ,能够获得的价值为 w i w_i wi 。求解将哪些物品放入背包可以使背包内物品价值总量最大,只需求出最大价值。

数据输入格式

第一行输入两个整数, N , V N,V N,V,用空格隔开分别表示物品的种类和背包的容积;
接下来有 N 行数据,每一行有两个整数 v i 和 w i v_i和w_i viwi ,分别表示第 i i i 件物品的体积和价值。

输出格式

输出一个整数,表示背包可以容纳物品的最大价值。

数据范围

0 < N , V < = 1000 0 < N,V <= 1000 0<N,V<=1000
0 < v i , w i < = 1000 0 < v_i,w_i <= 1000 0<vi,wi<=1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

8

解释

从物品中取出第二件和第三件物品放入背包,背包内的总价值最大为 8 。

1.2 基本思路

这是最基础的背包问题,其特点是:可以被选择的每种物品只有一件,可以选择放与不放。该特点也是01背包问题的关键,并且以后看到题目有这样的描述,那该题多半就是 01背包问题。

需要记录 d p [ i ] [ j ] dp[i][j] dp[i][j] 这样一个状态值, d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 种物品中随意挑选物品放在背包中,使背包内物品的总体积不能超过 j j j ,此时背包可以容纳的最大价值。题目要求的是面对这 N N N 种物品,我们可以随便挑选物品放在背包里,计算不超过背包可容纳体积时我们可以获得的最大价值。因此返回值就是 d p [ N ] [ V ] dp[N][V] dp[N][V]

状态转移方程为:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v i ] ) dp[i][j] = max(dp[i-1][j], dp[i-1][j-v_i]) dp[i][j]=max(dp[i1][j],dp[i1][jvi])
这个方程非常重要,也是接下来其他类型的背包问题的基础。我们来分析一下,为了获得最大背包容量下的最大物品价值,我们有两种选择的方案,就是面对第 i i i 件物品的时候,我们有如下两种选择,而最最大价值就是其中的较大者。

  • 选择1 放置第 i i i 件物品:此时就将该问题转化为 i − 1 i-1 i1 件物品放入容量为 j − v i j-v_i jvi 的背包 ,这时可以获得的最大价值就是 d p [ i − 1 ] [ j − w i ] dp[i-1][j-w_i] dp[i1][jwi] 再加上放入第 i i i 件物品的价值;
  • 选择2 不放置第 i i i 件物品:此时就将该问题转化为 i − 1 i-1 i1 件物品放入容量为 j j j 的背包 求最大价值即 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]

伪代码

d p [ 0 ] [ 0... V ] ← 0 dp\left[ 0 \right] \left[ 0...V \right] \gets 0 dp[0][0...V]0
f o r   i   ← 1   t o   N for\ i\ \gets 1\ to\ N for i 1 to N
f o r   j ← v i   t o   V \quad \quad for\ j\gets v_i\ to\ V for jvi to V
d p [ i ] [ j ]    ← max ⁡ ( d p [ i − 1 ] [ j ] ,    d p [ i − 1 ] [ j − v i ] + w i ) \quad \quad \quad \quad dp\left[ i \right] \left[ j \right] \,\,\gets \max \left( dp\left[ i-1 \right] \left[ j \right] ,\,\,dp\left[ i-1 \right] \left[ j-v_i \right] +w_i \right) dp[i][j]max(dp[i1][j],dp[i1][jvi]+wi)

1.3 空间上的优化

以上的求解方法时间复杂度和空间复杂度均是 O ( N V ) O(NV) O(NV),其中时间复杂度已经不能够优化看了,而空间复杂度还有优化的空间,可以优化到 O ( V ) O(V) O(V)

在上面的讲解思路中是有一个外层循环的 i ← 1... N i \leftarrow 1...N i1...N ,每次算出二维数组 d p [ i , 0... V ] dp[i, 0...V] dp[i,0...V] 的所有值。那么,如果只用一个数组 d p [ 0... V ] dp[0...V] dp[0...V] ,能不能保证计算完第 i i i 件物品的最大价值后 d p [ V ] dp[V] dp[V] 表示的就是我们定义的状态 d p [ i ] [ V ] dp[i][V] dp[i][V] 呢?根据 1.2 基本思路 中的描述可以知道,面对第 i i i 件物品, d p [ i ] [ j ] dp[i][j] dp[i][j] 是由 d p [ i − 1 ] dp[i-1] dp[i1] d p [ i − 1 ] [ j − v i ] dp[i-1][j-v_i] dp[i1][jvi] 两个子问题递推来的,如果我们维护一个 背包最大容量长度的数组 表示的是面对前 i − 1 i-1 i1 种物品的最大价值,即计算面对第 i i i 件物品时的最大价值, d p [ j ] dp[j] dp[j] 表示的是面对前 i − 1 i-1 i1 种物品背包容量是 j j j 的最大价值。

此时对于背包容量这一层该如何循环呢?这一层的循环以 j ← V   t o   0 j \leftarrow V\ to\ 0 jV to 0​ 递减的顺序计算 d p [ j ] dp[j] dp[j] ,这样才能保证计算 d p [ j ] dp[j] dp[j] d p [ j − v i ] dp[j-v_i] dp[jvi] 保存的是状态 d p [ i − 1 ] [ j − v i ] dp[i-1][j-v_i] dp[i1][jvi] 的值。

如果按照背包容量递增的顺序遍历会怎么样?面对第 i i i件物品 j j j 背包容量下的最大价值更新需要用到 d p [ i − 1 ,   0... V ] dp[i-1,\ 0...V] dp[i1, 0...V] 的数据。按照背包容量递增的顺序更新之后 d p [ j ] dp[j] dp[j] 表示的是 d p [ i , j ] dp[i, j] dp[i,j] ,而后面的 d p [ i , j . . . V ] dp[i, j...V] dp[i,j...V] 计算仍需要 d p [ i − 1 , . . . ] dp[i-1, ...] dp[i1,...] 的状态信息,因此这样按照背包容量增加的顺序更新数组会出错。于是这一层的循环是以 j ← V   t o   0 j \leftarrow V\ to\ 0 jV to 0 递减的顺序计算 d p [ j ] dp[j] dp[j] 的。

伪代码

d p [ 0... V ] ← 0 dp\left[ 0...V \right] \gets 0 dp[0...V]0
f o r   i   ← 1   t o   N for\ i\ \gets 1\ to\ N for i 1 to N
f o r   j ← V   t o   v i \quad \quad for\ j\gets V\ to\ v_i for jV to vi
d p [ j ]    ← max ⁡ ( d p [ j ] ,    d p [ j − v i ] + w i ) \quad \quad \quad \quad dp\left[ j \right] \,\,\gets \max \left( dp\left[ j \right] ,\,\,dp\left[ j-v_i \right] +w_i \right) dp[j]max(dp[j],dp[jvi]+wi)

1.4 算法实现

#include <iostream>

using namespace std;

#define maxn 1010

int N, V;				// 物品种类以及背包容量
int v[maxn], w[maxn];	// 物品的体积和价值数组
int dp[maxn][maxn];		// 背包内物品的最大价值,j体积下前i个物品最大价值

int get01BagMinVal() {
    for (int i = 1; i <= N; ++i) {
        for (int j = 1; j <= V; ++j) {
            // 当前背包容量可以装的下第i件物品,这时候有两种选择,取与不取
            if (j >= v[i]) {	
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
            }
            当前背包容量装不下第i件物品
            else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    
    return dp[N][V];
}

int main() {
    
    cin >> N >> V;
    for (int i = 1; i <= N; ++i) {
        cin >> v[i] >> w[i];
    }
    
    return 0;
}

// 以上给出的是没有进行空间优化的 01 背包求解

1.5 递归版本

递归方法需要维护一个背包剩余容量的整型变量,每次选择了当前物品时,就是要计算利用下一件及其以后物品的最大价值与不选择当前物品时下一件及其以后物品的最大价值进行比较,选择较大的值作为最大价值。

int maxValue(vector<int>& w, vector<int>& v, int bag) {
    return process(w, v, 0, bag);
} 
 
// rest 表示背包剩下的空间
// idx 及其往右的货物可以选择,但是注意不能超过 rest 的空间
int process(int>& w, vector<int>& v, int idx, int rest) {
    if (rest < 0) {				// base case 1
        return -1;
    }
    
    if (idx == w.size()) {		// base case 2
        return 0;
    }
    
    // 不选择 idx 位置的货物
    int p1 = process(w, v, idx + 1, rest);
    int p2 = -1;
    // 选择 idx 位置的货物
    int p2Next = process(w, v, idx + 1, rest - w[idx]);
    if (p2Next != -1) {
        p2 = v[idx] + p2Next;
    }
    
    return max(p1, p2);
}

1.6 按照递归修改的动态规划版本

有一种动态规划是可以根据递归方法修改得到的,递归里的 base case 对应的就是动态规划里的边界条件,而递归位置对应的就是动态规划里的转态转移。

int dpWay01(vector<int>& w, vector<int>& v, int bag) {
    int n = w.size();
    
    vector<vector<int>> dp(n+1, vector<int>(n+1));
    
    // dp[n][...] 对应暴力递归中的 base case 2
    for (int idx = n-1; idx >= 0; --idx) {
        for (int rest = 0; rest <= bag; ++rest) {
            dp[idx][rest] = dp[idx+1][rest];
            if (rest >= w[idx]) {
                dp[idx][rest] = max(dp[idx][rest], dp[idx + 1][rest - w[idx] + v[idx]);
            }                                               
        }
    }
                                     
    return dp[0][bag];
}

1.7 总结

01 背包问题是背包类问题的最基础的部分,包含了背包问题的设计状态方程的基本思想,需要理解透彻。别的类型的题目往往可以转化为 01 背包的问题进行求解。一定要仔细体会状态转移方程的意义以及空间优化方法。

空间优化方法中一定要好好理解 背包容量的倒叙遍历,以上的相关描述的层次可能还有点不清晰,还需要对陈述继续进行打磨,最好可以用一些生动的图示进行描述说明该问题。

2 完全背包

2.1 题目描述

N N N 种物品和一个容量为 V V V 的背包,每种物品都有无限件可以用。放入第 i i i 件物品的体积为 v i v_i vi ,能够获得的价值为 w i w_i wi 。求解将哪些物品放入背包可以使背包内物品价值总量最大,只需求出最大价值。

2.2 基本思路

完全背包问题和 01背包问题很像的,主要区别在于:每种物品都有无限件。从每种物品的角度考虑,此时的策略已经不是取与不取的问题了,而是这种物品取0次、1次,…, ⌊ V v i ⌋ \lfloor \frac{V}{v_i} \rfloor viV 次。

如果仍然按照 01 背包求解的思路,令 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 件物品放入一个容量为 j j j 的背包的最大价值。按照完全背包的物品拿取策略有这样的状态转移方程:
d p [ i ] [ j ] = max ⁡ { d p [ i − 1 ] [ j − k v i ] + k w i ∣ 0 ≤ k v i ≤ V } dp\left[ i \right] \left[ j \right] =\max \left\{ dp\left[ i-1 \right] \left[ j-kv_i \right] +kw_i|0\le kv_i\le V \right\} dp[i][j]=max{dp[i1][jkvi]+kwi∣0kviV}
按照这样求解的总的时间复杂度为 O ( N V ∑ V v i ) O(NV\sum{\frac{V}{v_i}}) O(NVviV),其中 ∑ j v i \sum{\frac{j}{v_i}} vij 表示的是所有物品可以拿取的总次数。
根据上述状态方程描述有这样的代码:

for (int i = 1; i <= N; ++i) {
    for (int j = 0; j <= V; ++j) {
    	for (int k = 0; k * v[i] <= j; ++k) {
			dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
		}
    }
}

2.3 优化一下

数学上的推导
将基本思路中的状态转移方程按 k k k 可能的取值拆开有:
d p [ i ,   j ] = max ⁡ { d p [ i − 1 ] [ j − k v i ] + k w i ∣ 0 ≤ k v i ≤ V } = max ⁡ { d p [ i − 1 ] [ j ] ,   d p [ i − 1 ] [ j − v i ] + w i ,   d p [ i − 1 ] [ j − 2 v i ] + 2 w i ,   . . . ,   d p [ i − 1 ] [ j − k v i ] + k w i } dp\left[ i,\ j \right] =\max \left\{ dp\left[ i-1 \right] \left[ j-kv_i \right] +kw_i|0\le kv_i\le V \right\}= \\ \max \left\{ dp\left[ i-1 \right] \left[ j \right] ,\ dp\left[ i-1 \right] \left[ j-v_i \right] +w_i,\ dp\left[ i-1 \right] \left[ j-2v_i \right] +2w_i,\ ...,\ dp\left[ i-1 \right] \left[ j-kv_i \right] +kw_i \right\} dp[i, j]=max{dp[i1][jkvi]+kwi∣0kviV}=max{dp[i1][j], dp[i1][jvi]+wi, dp[i1][j2vi]+2wi, ..., dp[i1][jkvi]+kwi}

j = j − v i j = j-v_i j=jvi 带入上式有:
d p [ i ,   j − v i ] + w i = max ⁡ { d p [ i − 1 ] [ j − v i − m v i ] + k w i ∣ 0 ≤ ( m + 1 ) v i ≤ V } + w i = max ⁡ { d p [ i − 1 ] [ j − v i ] + w i ,   d p [ i − 1 ] [ j − 2 v i ] + 2 w i ,   . . . ,   d p [ i − 1 ] [ j − ( m + 1 ) v i ] + ( m + 1 ) w i } dp\left[ i,\ j-v_i \right] +w_i=\max \left\{ dp\left[ i-1 \right] \left[ j-v_i-mv_i \right] +kw_i|0\le \left( m+1 \right) v_i\le V \right\} +w_i=\\ \max \left\{ dp\left[ i-1 \right] \left[ j-v_i \right] +w_i,\ dp\left[ i-1 \right] \left[ j-2v_i \right] +2w_i,\ ...,\ dp\left[ i-1 \right] \left[ j-\left( m+1 \right) v_i \right] +\left( m+1 \right) w_i \right\} dp[i, jvi]+wi=max{dp[i1][jvimvi]+kwi∣0(m+1)viV}+wi=max{dp[i1][jvi]+wi, dp[i1][j2vi]+2wi, ..., dp[i1][j(m+1)vi]+(m+1)wi}

以上两式中 k = = m + 1 k == m+1 k==m+1 的,表示的都是最大背包容量下可放置当前物品 i i i 的最大数量,即 k = = m + 1 = = ⌊ V v i ⌋ k==m+1== \lfloor \frac{V}{v_i} \rfloor k==m+1==viV 。因此状态转移方程可以优化为:
d p [ i ] [ j ] = max ⁡ { d p [ i − 1 ] [ j ] ,   d p [ i − 1 ] [ j − v i ] + w i } dp\left[ i \right] \left[ j \right] =\max \left\{ dp\left[ i-1 \right] \left[ j \right] ,\ dp\left[ i-1 \right] \left[ j-v_i \right] +w_i \right\} dp[i][j]=max{dp[i1][j], dp[i1][jvi]+wi}
逻辑上的理解
完全背包问题中的每种物品都可以无限使用的,在考虑 “再选一个第 i i i 件物品” 这种策略时,正需要一个可能已经选入第 i i i 种物品的子结果 d p [ i ] [ j − v i ] dp[i][j-v_i] dp[i][jvi] ,所以这里内存循环的顺序也就是按照背包容量增加的顺序了。

通过以上分析,完全背包中的k循环可以被替换掉,于是有:

for (int i = 1; i <= N; ++i) {
    for (int j = 0; j <= V; ++j) {
    	dp[i][j] = dp[i-1][j];
    	if (j >= v[i]) {
    		dp[i][j] = max(dp[i][j], dp[i][j-v[i]] + w[i]);
    	}
    }
}

根据以上代码按照01背包的优化思路又可以使用一维动态数组进行如下优化,注意这里的内层循环是按照背包容量增加的顺序进行遍历。

for (int i = 1; i <= N; ++i) {
    for (int j = 0; j <= V; ++j) {
    	dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
    }
}

伪代码

d p [ 0... V ] ← 0 dp\left[ 0...V \right] \gets 0 dp[0...V]0
f o r   i   ← 1   t o   N for\ i\ \gets 1\ to\ N for i 1 to N
f o r   j ← v i   t o   V \quad \quad for\ j\gets v_i\ to\ V for jvi to V
d p [ j ]    ← max ⁡ ( d p [ j ] ,    d p [ j − v i ] + w i ) \quad \quad \quad \quad dp\left[ j \right] \,\,\gets \max \left( dp\left[ j \right] ,\,\,dp\left[ j-v_i \right] +w_i \right) dp[j]max(dp[j],dp[jvi]+wi)

算法复杂度

时间复杂度: O ( N V ) O(NV) O(NV)
空间复杂度: O ( V ) O(V) O(V)

2.4 代码实现

#include <iostream>

using namespace std;

#define maxn 1010

int N, V;				// 物品种类以及背包容量
int v[maxn], w[maxn];	// 物品的体积和价值数组
int dp[maxn][maxn];		// 背包内物品的最大价值,j体积下前i个物品最大价值

int getCompleteBagMinVal() {
    for (int i = 1; i <= N; ++i) {
        for (int j = 0; j <= V; ++j) {
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
    }
    
    return dp[N][V];
}

int main() {
    
    cin >> N >> V;
    for (int i = 1; i <= N; ++i) {
        cin >> v[i] >> w[i];
    }
    
    return 0;
}

2.5 总结

完全背包也是背包系列问题的基础问题。在这类问题中,第一个状态转移方程是比较容易理解的,优化后的状态转移方程相对不容易理解,并且内层循环容易受 01背包问题影响出错。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【背包问题】之01背包和完全背包 的相关文章

随机推荐

  • Java生成一个包含所有数字大小写字母的集合

    利用循环给集合添加所有数字和字母 import java util ArrayList ArrayList
  • steam移动所有文件至新库文件夹失败_不想次次重复下载游戏?手把手教你打造移动游戏、软件库...

    大家好 我是黄昏百分百 之前我有幸获邀参与了七彩虹RTX 3080显卡的媒体首测 在测试过程中 我购买了很多的3A大作 下着下着游戏 就发现固态硬盘不够用了 所以 我觉得升级我的固态硬盘 于是 就有了今天这篇如何通过固态硬盘打造移动游戏 软
  • 【Android自动化测试】5个必备的测试框架

    Appium Appium是一个开源的移动测试工具 支持iOS和Android 它可以用来测试任何类型的移动应用 原生 网络和混合 作为一个跨平台的工具 你可以在不同的平台上运行相同的测试 为了实现跨平台的功能 Appium使用了供应商提供
  • C++析构函数调用异常问题研究

    最近又遇到一个奇葩问题 程序在自己的开发机器和某些机器上运行完好 但是在测试人员的几台机器上运行就直接推出了 开始以为是出现了野指针 因为delete野指针时程序会直接退出 代码翻来覆去过来即便确认没有野指针后问题就陷入了死循环 经过多次调
  • mysql超时连接问题

    一 问题 nested exception is com mysql jdbc exceptions jdbc4 CommunicationsException Communications link failure The last pa
  • 28岁,从字节退休了···

    大厂一直是每个程序员都向往职业目标 大厂意味着薪资高 福利好 倍有面儿 而且发展空间也大 甚至有人调侃不想进大厂的程序员不是好程序员 而在网上 也有各个网友分享自己在大厂的经历 在某平台还有一个近2600万浏览的话题 在字节跳动工作是怎么样
  • jvm堆大小的设置

    问题引入 Xmx10240m Xms10240m Xmn5120m XXSurvivorRatio 3 其最小内存值和Survivor区总大小分别是 10240m 2048m 解析 Xmx 最大堆大小 Xms 初始堆大小 Xmn 年轻代大小
  • es6常见的相关问题

    文章目录 ES6常见的相关问题 1 var let const之间的区别 1 var 注意点1 变量提升 注意点2 块级作用域 注意点3 重复声明 2 let ES6 注意点1 变量提升 不存在 注意点2 块级作用域 注意点3 重复声明 注
  • ICCV2019 oral papers

    ICCV2019 oral papers Exploring Randomly Wired Neural Networks for Image Recognition Paper Video Progressive Differentiab
  • 10个良心网站推荐

    对效率工具感兴趣的可以看一看我的往期视频 10个良心网站推荐第三期 10个良心网站推荐 10个良心网站推荐第二期 av76255938 13个逆天网站工具 av70359966 10个改变生活的网站 10个改变生活的网站 本期推荐 1 Vi
  • ctf从零开始学0x02 ctf的逆向reverse 常见思路和如何入门(文末)

    逆向是啥 很多人一开始都感到很糊涂 下面给出相关的概念 对于相关如何打逆向的基础可以看 https blog csdn net qq 43504939 article details 90246409 其中 80 的题目都与crypto结合
  • 005-1基于深度优先搜索查找图中连通路径

    基于图的深度优先搜索 图学习笔记索引 本文参考 算法 第4版 基于图的深度优先搜索 1 自定义输入流In 2 定义背包类Bag 3 无向图G的构造 4 深度优先搜索DepthFirstSearch 5 使用深度优先搜索查找连通路径Depth
  • Win7 X86上搭建Android开发环境

    Android SDK有Mac Linux和Windows三个平台的版本 处于个人习惯 决定在Win7 X86的电脑上搭建Android开发环境 windows平台搭建Android开发环境 需要以下相关安装包 1 JDK6 需下载并安装
  • Apollo 应用与源码分析:CyberRT-protobuf

    目录 概念 特点 优点 缺点 文件的创建 1 字段规则 2 数据类型 3 字段名称 4 字段编号 文件的编译 protobuf 编译命令编译 protobuf cmake 方式编译 使用bazel 编译 在protobuf 文件夹下创建bu
  • python实现超市商品销售管理系统

    class Goods object def init self id name price self id id self name name self price price def str self info 编号 s t商品名称 s
  • 使用VirtualBox安装Ubuntu虚拟机 - 完整教程

    前言 本教程将演示通过 VirtualBox 安装 Ubuntu 请提前下载好以下文件哦 VirtualBox 软件 Ubuntu 的 镜像文件 iso 下载地址 VirtualBox 版本 VirtualBox 7 0 2 官网链接 ht
  • eclipse常用快捷键

    Eclipse中10个最有用的快捷键组合 一个Eclipse骨灰级开发者总结了他认为最有用但又不太为人所知的快捷键组合 通过这些组合可以更加容易的浏览源代码 使得整体的开发效率和质量得到提升 1 ctrl shift r 打开资源 这可能是
  • 如何安装iGG

    优点 1 安装 iGG 不能访问视频网站 但可以支持GG学术 GG搜索进行学术研究 2 作为GG扩展程序客户端只用登陆一次 非常便捷 3 性价比高 充值会员手机端也可以安装使用 步骤
  • 学习大数据所需的关键知识

    在人工智能领域 学习大数据技术是一项重要的任务 随着数据的快速增长和各行各业对数据的需求不断增加 掌握大数据技术可以帮助我们有效地处理 存储和分析海量数据 本文将介绍学习大数据所需的关键知识 并提供相应的源代码示例 数据存储和处理 在学习大
  • 【背包问题】之01背包和完全背包

    文章目录 1 01 背包 1 1 题目描述 1 2 基本思路 1 3 空间上的优化 1 4 算法实现 1 5 递归版本 1 6 按照递归修改的动态规划版本 1 7 总结 2 完全背包 2 1 题目描述 2 2 基本思路 2 3 优化一下 2