问题描述如下,现在有N件物品,以及一个容量为V的背包,每个物品都有他们的价值,现在的问题就是在你背包装的下的情况下,怎么拿这些物品,使得你拿的物品的价值最大,其中每件物品你只能选择拿或者不拿。

image-20220918211331405

  上面的问题就是非常经典的"0,1"背包问题,对于这样的问题,我们通常可以使用动态规划(Dynamic Programming)来求解,动态规划的思路就是找到一个状态转移的表达式来帮助我们进行求解,那么对于“0,1”背包问题,我们这样来思考,如下图所示。

image-20220920151435077

  根据上图的描述,我们找到了“0,1”背包的转态转移表达式,接下来需要关注的是我们枚举的顺序,很明显,我们的物品数量肯定是从少到多来枚举,背包容量也是从小到大来枚举,因为根据上面的转态转移方程,很容易看出当前状态是需要之前转态的值的,所以我们从小到大进行枚举物品数目以及背包容量。这样我们第一个版本的代码如下:

#include<iostream>
using namespace std;
int main(){
    int n = 0,v = 0;
    // 得到物品总数目n以及背包容量v
    cin>>n>>v;
    // 定义物品的容量数组和价值数组
    int volume[n+1],value[n+1];
    // 定义dp数组 dp[i][j]表示在背包容量为j 物品有i个 该种情况下能拿到的最大价值
    int dp[n+1][v+1];
    for(int i = 0;i<=n;i++){
        for(int j = 0;j<=v;j++){
            dp[i][j] = 0;
        }
        if(i>=1){
            cin>>volume[i]>>value[i];
        }
    }
    for(int i = 1;i<=n;i++){
        // 第一层循环 枚举物体数目 物体数目从1开始
        for(int j = 0;j<=v;j++){
            // 第二层循环 枚举背包空间 背包空间可以从0开始
            dp[i][j] = dp[i-1][j];
            if(j>=volume[i]){
                // 若当前背包容量大于物品的体积
                dp[i][j] = max(dp[i][j],dp[i-1][j - volume[i]]+value[i]);
            }
        }
    }
    cout<<dp[n][v]<<endl;
    return 0;
}

  这样,我们完成了“0,1”背包,上面的代码通常是有优化的方式的,我们通过状态压缩,能够将二维的dp数组,压缩成一维的dp数组。从上面的代码中,我们可以看出,转态转移过程中只用到了i与i-1,那么我们便没必要使用i,可以进行状态压缩,压缩成一维,如下图所示:

image-20220920173116352

  代码如下所示:

#include<iostream>
using namespace std;
int main(){
    int n = 0,v = 0;
    // 得到物品总数目n以及背包容量v
    cin>>n>>v;
    // 定义物品的容量数组和价值数组
    int volume[n+1],value[n+1];
    // 定义dp数组 dp[j]表示背包容量为j的情况下的最大价值
    int dp[v+1];
    for(int j=0;j<=v;j++){
        dp[j] = 0;
    }
    for(int i = 1;i<=n;i++){
        cin>>volume[i]>>value[i];
    }
    for(int i = 1;i<=n;i++){
        // 第一层循环 枚举物体数目 物体数目从1开始
        for(int j = v;j>=volume[i];j--){
            // 第二层循环 枚举背包空间 背包空间从最大的开始
            // 因为每次更新dp[j]的时候,我们使用到的是i-1的dp[j]与dp[j - volume[i]] + value[i]
            // 如果顺序枚举的话 dp[j-volume[i]]会先被更新 得到 第i层的 dp[j-volume[i]] 那么在更新较大的dp[j]的时候
            // 我们会使用到 dp[j - volume[i]]但是 我们需要的是 i-1的 dp[j-volume[i]]而不是更新后的 第i层的 dp[j-volume[i]]
            dp[j] = max(dp[j],dp[j - volume[i]]+value[i]);
        }
    }
    cout<<dp[v]<<endl;
    return 0;
}

  那么到这里,我们就弄懂了“0,1”背包。接下来找一些题目练练手,题目列表如下:

1. 最后一块的石头重量2 题号:1049 链接:https://leetcode.cn/problems/last-stone-weight-ii/ 隐性的01背包
2. 分割等和子集 题号: 416 链接:https://leetcode.cn/problems/partition-equal-subset-sum/ 隐性的01背包
3. 目标和 题号:494 链接:https://leetcode.cn/problems/target-sum/submissions/ 01背包 需要背包完全装满的方案数目

Q.E.D.


 研究僧一名,CV领域,研究方向为对抗攻击,欢迎各位前来交流