LintCode 440: Backpack III (完全背包问题,DP经典)

Question
Given n kind of items with size Ai and value Vi( each item has an infinite number available) and a backpack with size m. What’s the maximum value can you put into the backpack?
Notice
You cannot divide item into small pieces and the total size of items you choose should smaller or equal to m.
Example
Given 4 items with size [2, 3, 5, 7] and value [1, 5, 2, 4], and a backpack with size 10. The maximum value is 15.

解法1:
将其视为多重背包变形,每种物品取的上限是m/A[i]。
注意:

  1. 循环j和循环k可互换。
  2. 循环k必须从0开始,因为对某个物品,也可以不用。注意对2维矩阵的方案,k都必须从0开始,跟多重背包一样。
class Solution {
public:
    /**
     * @param A: an integer array   // size array
     * @param V: an integer array   // value array
     * @param m: An integer         // backpack size
     * @return: an array
     */
    int backPackIII(vector<int> &A, vector<int> &V, int m) {
        int n = A.size();
        //dp[i][j] : maximum value of first i items with bag size j
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
        
        for (int i = 1; i <= n; ++i) {
            for (int j = A[i - 1]; j <= m; ++j) {  
                for (int k = 0; k * A[i - 1] <= m; ++k) { //对2维矩阵的方案,k都必须从0开始,跟多重背包一样
                    if (j >= k * A[i - 1])
                        dp[i][j] = max(dp[i][j], dp[i - 1][j - k * A[i - 1]] + k * V[i - 1]);
                }
            }
        }
        
        return dp[n][m];
    }
};

解法2:解法1的空间简化版。
时间复杂度: O(sum(m/A[i]) * backpackSize)。
空间复杂度: O(m)。

class Solution {
public:
    /**
     * @param A: an integer array   // size array
     * @param V: an integer array   // value array
     * @param m: An integer         // backpack size
     * @return: an array
     */
    int backPackIII(vector<int> &A, vector<int> &V, int m) {
        int n = A.size();
        vector<int> dp(m + 1, 0);
        
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j * A[i - 1] <= m; ++j) {  //j can start from 0 or 1
                for (int k = m; k >= A[i - 1]; --k) {
                    dp[k] = max(dp[k], dp[k - A[i - 1]] + V[i - 1]); //当前item不取,或至少取一个
                }
            }
        }
        
        return dp[m];
    }
};

// The following code is also OK. A[i - 1] to A[i]
#if 0
    int backPackIII(vector<int> &A, vector<int> &V, int m) {
        int n = A.size();
        vector<int> dp(m + 1, 0);
        for (int i = 0; i < n; ++i) {
            for (int j = 1; j * A[i] <= m; ++j) {  //j can start from 0 or 1
                for (int k = m; k >= A[i]; --k) {
                    dp[k] = max(dp[k], dp[k - A[i]] + V[i]);
                }
            }
        }
        
        return dp[m];
    }
#endif

注意:
1)这里1维数组的优化方案的j 循环可以从0开始,也可以从1开始,即:

for (int j = 1; j * A[i] <= m; ++j) 

for (int j = 0; j * A[i] <= m; ++j) 

都可以。
而在多重背包问题的1维数组的优化方案中,j循环必须从1开始,即:

 for (int j = 1; j <= amounts[i]; ++j) {

我想这个原因是因为完全背包问题中,每个物品可以任意多个,所以任何dp[k - A[i]]的值是肯定有意义的(实际上只要k>=A[i], dp[k-A[i]]的值都有意义,都代表当容量为k-A[i]时的最优值。

而多重背包问题中,每个物品的数目是有限的。某些dp[k-A[i]]不一定对。如果j从0开始,会导致这件物品可以取amounts[i]+1件。这是不对的。

解法2:
3重循环变2重循环:去掉j循环,将k循环反过来,从小到 大查找即可。
时间复杂度O(mn),空间复杂度O(m)。

    int backPackIII(vector<int> &A, vector<int> &V, int m) {
        int n = A.size();
        vector<int> dp(m + 1, 0);
        
        for (int i = 0; i < n; ++i) {
            for (int k = A[i]; k <= m; ++k) {    
                dp[k] = max(dp[k], dp[k - A[i]] + V[i]);
            }
        }
        return dp[m];
    }

注意:

  1. 只有完全背包问题可以用Solution 2这种优化,而多重背包不可以。
    为什么呢?我们看下面这个循环:
         for (int k = prices[i]; k <= m; ++k) {
             dp[k] = max(dp[k], dp[k - prices[i]] + weight[i]);
         }

在完全背包问题中,每个物品可以取任意多件,所以dp[k - prices[i]] + weight[i]]永远是有意义的。
而在多重背包问题中,每个物品的个数有上限,所以dp[k - prices[i]] + weight[i]]不一定有意义。它可能是某个物品多取了几项后的dp值,但不符合题目要求了。

那这种优化能应用于01背包吗? 也不行。以01背包代码为例:

我们假设j从小到大循环,即:

    int backPack(int m, vector<int> &A) {
        int n = A.size();
        vector<int> dp(m + 1, 0);
        
        for (int i = 0; i < n; ++i) {
            //for (int k = m; k >= A[i]; --k) { //正确
            for (int k = A[i]; k <= m; ++k) {  //错误
                dp[k] = max(dp[k], dp[k - A[i]] + A[i]);
                cout<<"i="<<i<<" k="<<k<<" "<<dp[k]<<endl;
            }
        }
        cout<<endl;
        return dp[m];
    }

假设输入为:
10
[3,4,8,5]
则打印信息为
i=0 k=3 3
i=0 k=4 3
i=0 k=5 3
i=0 k=6 6 //error starts from here!
i=0 k=7 6
i=0 k=8 6
i=0 k=9 9
i=0 k=10 9
i=1 k=4 4
i=1 k=5 4
i=1 k=6 6
i=1 k=7 7
i=1 k=8 8
i=1 k=9 9
i=1 k=10 10
i=2 k=8 8
i=2 k=9 9
i=2 k=10 10
i=3 k=5 5
i=3 k=6 6
i=3 k=7 7
i=3 k=8 8
i=3 k=9 9
i=3 k=10 10
dp[]=0 0 0 3 4 5 6 7 8 9 10

可见当i=1, k=10, dp[10] = max(dp[10], dp[10 - A[1]] + A[1]);
而A[1]=4, 所以dp[10] = max(dp[10], dp[6] + 4);
此时dp[6]=6,dp[10]=9, 所以dp[10]=10。
但是我们知道答案应该是9=4+5。原因在于当i=0时,只有k=3,4,5时是对的,k=6时就错了。
dp[6] = max(dp[6], dp[6 - 3] + 3); 此时dp[6]的值是item 0取了2次,而item 0总共才1个。注意如果是完全背包就无所谓了,反正可以取任意多个(只要不超过包大小)。

那么为什么k从大到小就对了呢?
我们把前面的k循环变成从大到小,看看打印信息:
i=0 k=10 3
i=0 k=9 3
i=0 k=8 3
i=0 k=7 3
i=0 k=6 3
i=0 k=5 3
i=0 k=4 3
i=0 k=3 3
i=1 k=10 7
i=1 k=9 7
i=1 k=8 7
i=1 k=7 7
i=1 k=6 4
i=1 k=5 4
i=1 k=4 4
i=2 k=10 8
i=2 k=9 8
i=2 k=8 8
i=3 k=10 9
i=3 k=9 9
i=3 k=8 8
i=3 k=7 7
i=3 k=6 5
i=3 k=5 5
dp[] = 0 0 0 3 4 5 5 7 8 9 9
我们可以看出,当我们采用一维数组的优化方案时,k从大到小取,可以保证dp[k]里面的值是每个item只取了一个的时候的最优值。所以01背包和多重背包的k循环只能从大到小取,而完全背包的k循环要从小到大。这是01背包/多重背包和完全背包的一个重要区别!

补充一下:正序和逆序的区别就是,对于正序,j以前的状态是当前行的状态;对于逆序,j以前的状态是上一个行的状态。
逆序循环: dp[i][k] 是由 dp[i - 1][k - A[i] - 1] + A[i - 1])得来,逆序保证了每个物品只被运用一次。
正序循环: dp[i][k] 是由 dp[i][k - A[i] - 1] + A[i - 1])得来,正序保证了每个物品可被运用多次。

再补充一点 //下面的j就是上面的k,偷懒没换。
01背包的转移方程是:
d p [ i ] [ j ] = m a x { d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] } dp[i][j]=max \{dp[i−1][j], dp[i−1][j−v[i]]+w[i] \} dp[i][j]=max{dp[i1][j],dp[i1][jv[i]]+w[i]}
​其中, d p [ i − 1 ] [ j ] dp[i−1][j] dp[i1][j]表示没选第 i i i种, d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] dp[i-1][j−v[i]]+w[i] dp[i1][jv[i]]+w[i]表示选了第 i i i

完全背包的转移方程是:
d p [ i ] [ j ] = m a x { d p [ i − 1 ] [ j ] , d p [ i ] [ j − v [ i ] ] + w [ i ] } dp[i][j]=max \{dp[i−1][j], dp[i][j−v[i]]+w[i] \} dp[i][j]=max{dp[i1][j],dp[i][jv[i]]+w[i]}
其中, d p [ i − 1 ] [ j ] dp[i−1][j] dp[i1][j]表示没选第i种, d p [ i ] [ j − v [ i ] ] + w [ i ] dp[i][j−v[i]]+w[i] dp[i][jv[i]]+w[i]表示选了若干个第 i i i

我们可以看出,唯一的区别就是01背包用了 d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] dp[i−1][j−v[i]]+w[i] dp[i1][jv[i]]+w[i]而完全背包用 d p [ i ] [ j − v [ i ] ] + w [ i ] dp[i][j−v[i]]+w[i] dp[i][jv[i]]+w[i].
01背包的dp[i]是基于dp[i-1],即上一轮的值。这就是为什么01背包优化循环要从大到小!
完全背包的dp[i]是基于dp[i],即本轮的值。这就是为什么完全背包优化循环要从小到大!

代码同步在
https://github.com/luqian2017/Algorithm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值