1. 程式人生 > >LintCode 799: Backpack VIII (多重揹包問題變種,DP經典題, 難!)

LintCode 799: Backpack VIII (多重揹包問題變種,DP經典題, 難!)

我開始覺得這就是多重揹包問題的變種,但是解法1會超時。因為有3重迴圈,所以當amount[i]的值都很大就超時了。

解法1:
dp[i]表示coins是否可以組合成i的值。

class Solution {
public:
    /**
     * @param n: the value from 1 - n
     * @param value: the value of coins
     * @param amount: the number of coins
     * @return: how many different value
     */
    int backPackVIII(int n, vector<int> &value, vector<int> &amount) {
        int m = value.size();
        vector<bool> dp(n + 1, false); //dp[i] shows if the coins can be combined into i
        int sum = 0;
        
        dp[0] = true;
        for (int i = 0; i < m; ++i) {
            for (int j = 1; j <= amount[i]; ++j) {
                for (int k = n; k >= value[i]; --k) {
                    if (!dp[k] && dp[k - value[i]]) { //記得!dp[k]可以剪枝
                        dp[k] = true;
                    }
                }
            }
        }    
            
        for (int i = 1; i <= n; ++i) {
            if (dp[i]) sum++;
        }
        
        return sum;
    }
};

解法2(優化):
優化非常巧妙,不容易想到。我參考了九章答案。
首先我們來看這道題跟傳統的多重揹包有什麼區別? 傳統的多重揹包我們需要比大小。我們來看這backPackVII的程式碼(見我的部落格LintCode 798)。

      int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
        int itemCount = prices.size();   //count of items
        vector<int> dp(n + 1, 0);
        for (int i = 0; i < itemCount; ++i) {
            for (int  j = 1; j < amounts[i]; ++j) {
                for (int k = n; k >= prices[i]; --k) {
                    dp[k] = max(dp[k], dp[k - prices[i]] + weight[i]);
                 }
             }
        }
        return dp[n];
    }

dp[k]是求價值不超過k情況下的最大重量。具體的說就是比較當前的dp[k]和第i-1次的還沒有裝item i時候的價格為k-prices[i]時的最大重量加上item i時候的重量,因為item i有amount[i]個,所以我們一定要比較amount[i]次才能找到最大值。

而對於這道題目Backpack VIII,我們的目標只是求dp[k]是true還是false。我們將dp[1…n]的初始值設為false。什麼時候我們需要dp[k]為真呢? 當下面3個條件同時成立時:

  1. 當dp[k]為false。這是顯然的剪枝條件。當dp[k]為true,我們不需要做重複功。
  2. 當dp[k - value[i]]為真。此時說明這些硬幣可以湊成k - value[i]的值。如果dp[k - value[i]]為假,說明dp[k]也是false。因為在前
    i-1次迴圈中(即前i-1種硬幣不能湊出k-value[i]的錢,說明加上value[i]也湊不出k的錢)。
  3. 當前i次迴圈的時候,所用的硬幣i的數量還沒超過amount[i]。這也是顯然的。注意這裡我們必須要用另外一個數組
    count[]。count[x]即當前(i)的時候,多少 個硬幣i已經用了來和其他硬幣湊出x的價值。

程式碼如下:

class Solution {
public:
    /**
     * @param n: the value from 1 - n
     * @param value: the value of coins
     * @param amount: the number of coins
     * @return: how many different value
     */
    int backPackVIII(int n, vector<int> &value, vector<int> &amount) {
        int m = value.size();
        vector<bool> dp(n + 1, false); //dp[i] shows if the coins can be combined into i
        int sum = 0;
        
        dp[0] = true;
        for (int i = 0; i < m; ++i) {
            vector<int> count(n + 1, 0);  //count[x] is for current i, how many i s have been used to get x
            for (int k = value[i]; k <= n; ++k) {
                if (!dp[k] && dp[k - value[i]] && count[k - value[i]] < amount[i]) {
                    dp[k] = true;
                    count[k] = count[k - value[i]] + 1;
                    sum ++;
               }
           }
       }    
       return sum;
    }
};

注意:

  1. 為什麼這裡k迴圈又是從小到大呢?而相比之下為什麼Backpack VII裡面的優化方案的k迴圈是從大到小?

注意在Backpack VII裡面,

dp[k] = max(dp[k], dp[k - prices[i]] + weight[i]);

也就是i時候的時候dp[k]實際上依賴於i-1時候的dp[k-prices[i]]。
所以對於大的k值的dp[k]依賴於上輪迴圈的小的k的dp[k]值,所以k迴圈從大到小。這裡為什麼還需要j迴圈呢?看程式碼

 for (int  j = 1; j < amounts[i]; ++j) {
    for (int k = n; k >= prices[i]; --k) {
          dp[k] = max(dp[k], dp[k - prices[i]] + weight[i]);
    }
}

假設對於i=2, amount[2]=4, k=8, prices[2]=3, weight[2]=1,則k迴圈一輪之後 dp[8]=max(dp[8], dp[5]+1)。因為k從大到小,所以後面的dp[5]變了dp[8]不知道,但是因為amount[2]=4,即item2有4個,我們再來一次k迴圈之後dp[8]就會被更新的dp[5]更新了。用完所有的amount[2]之後,dp[8]就是到i時候的最優值了。

而Backpack VIII裡面,i時候的dp[k]為真依賴於i-1時候的dp[k - value[i ]]為真, 而i-1時候的dp[k-value[i]]為真,i時候的dp[k-value[i]]也必然為真, 因為dp值一旦為真就永遠不會變了!。所以我們這裡不需要用到上輪迴圈的值,所以k的值可以從小到大,也不需要j迴圈。

如果這裡將k的值變成從大到小,就必須加j迴圈,否則不對。假設dp[8]的依賴於dp[5]的值,而dp[5]的值又依賴於dp[3]的值,等等,只有一層迴圈是不能取到最優的。