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個條件同時成立時:
- 當dp[k]為false。這是顯然的剪枝條件。當dp[k]為true,我們不需要做重複功。
- 當dp[k - value[i]]為真。此時說明這些硬幣可以湊成k - value[i]的值。如果dp[k - value[i]]為假,說明dp[k]也是false。因為在前
i-1次迴圈中(即前i-1種硬幣不能湊出k-value[i]的錢,說明加上value[i]也湊不出k的錢)。 - 當前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;
}
};
注意:
- 為什麼這裡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]的值,等等,只有一層迴圈是不能取到最優的。