01背包问题


01背包问题

来源

https://www.acwing.com/problem/content/2/

Description

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

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

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

Input

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

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

Output

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

数据范围

0

Sample Input

4 5
1 2
2 4
3 4
4 5

Sample Output

8

分析问题:

  1. 01背包问题的特点:每个物品只有一件,只有取或者不取两种状态

  2. 由于背包问题是dp的一个大类,所以我们也这里采用dp的逻辑框架来思考问题:

    • 状态表示:dp[i][j]表示在包括前i种物品,容积为j情况下的最大价值

    • 状态转移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])

      MOOC的陈越老师说过越简单的算法包含的信息量往往越大,第一次看到这个方程先别晕,我们来慢慢将它解释清楚.

      由于状态转移方程代表的两个状态之间的联系,所以让我们思考一下到dp[i][j]有哪些“路径”:

      • 显然每个物品的状态都有拿/不拿两种状态
      • dp[i-1][j]:先思考不拿的情况,显然dp[i][j]的状态可以直接由dp[i-1][j]得到
      • dp[i-1][j-w[i]]+v[i]:再思考的情况,此时我们需要先保证对应的体积是满足j-w[i]>0的,对应的上一个状态也不应该是简单的dp[i-1][j-1],而是dp[i-1][j-w[i]],此时再加上当前物品的价值即可

      一个明显的事实是转态转移的过程应该是从无到有,对应的下标也应该是从小到大的,

      所以我们这里也是从对应的这两个维度去讨论.

    • 初始化:由于dp[0][0]代表的是0种物品,0体积下的最大价值,所以初值应该为0

    #include 
    #include 
    #include 
    #include 
    #include 
    #define INF 0x3f3f3f3f
    
    using namespace std;
    
    const int N=1e3;
    int dp[N+10][N+10],w[N],v[N];  //dp[i][j]表示到第i种商品,恰好在j体积下的最大价值
    
    int main()
    {
        int n,m;
        while(~scanf("%d %d",&n,&m)){
            memset(dp,0,sizeof dp);
            for(int i=1;i<=n;i++){
                scanf("%d %d",&w[i],&v[i]);
            }
            for(int i=1;i<=n;i++){
                for(int j=1;j<=m;j++){  //由于dp的定义,我们显然也要保证j=w[i]){
                        dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+v[i]);  //两种状态:拿,或者不打
                    }
                }
            }
            cout << dp[n][m] << endl;
        }
    }
    
  3. 思考一下在dp的框架下如何优化这个问题,我们是否可以将状态方程由二维转为一维:

    • 观察上式,我们可以看出dp[i][j]i维上的状态始终只与i-1维有关,所以我们可以对其进行优化

    • 状态表达式:dp[j]表示i体积下的最大价值

    • 状态转移方程:dp[j]=max(dp[j],dp[j-w[i]]+v[i])

      注:关于这里为什么dp[j],大家可以对二维状态表达式方法做一个等价变换以帮助理解

    • 初始化:dp[0]=0

    • 注意点:和上一题不同的是,在二维存储的状态下,我们可以通过i-1保证对于第i件的一次取或不取的操作.但是在一维状态,我们无法确保当前操作的dp[j]是否已包含了第i件物品.

      我们以一次初始化为例查看这个问题(字丑,勿怪):

      image-20210122102805099

      解决:将j从大到小枚举

      image-20210122103103387

    #include 
    #include 
    #include 
    #include 
    #include 
    #define INF 0x3f3f3f3f
    
    using namespace std;
    
    const int N=1e3;
    int dp[N+10],w[N],v[N];  //dp[i]表示到i体积下的最大价值
    
    int main()
    {
        int n,m;
        while(~scanf("%d %d",&n,&m)){
            memset(dp,0,sizeof dp);
            for(int i=1;i<=n;i++){
                scanf("%d %d",&w[i],&v[i]);
            }
            for(int i=1;i<=n;i++){
                for(int j=m;j>=w[i];j--){  //保证了0/1的特性
                    dp[j]=max(dp[j],dp[j-w[i]]+v[i]);  //注意到在求max的时候,体积维度的值始终是不需要通过-1初始化的
                }
            }
            cout << dp[m] << endl;
        }
    }