圖解二維完全揹包問題——降維打擊

真昼小天使daisuki發表於2024-03-25

例題

例題:518. 零錢兌換 II

概述:

給你一個整數陣列 coins 表示不同面額的硬幣,另給一個整數 amount 表示總金額。
請你計算並返回可以湊成總金額的硬幣組合數。如果任何硬幣組合都無法湊出總金額,返回 0 。
假設每一種面額的硬幣有無限個。
題目資料保證結果符合 32 位帶符號整數。

樸素的二維完全揹包

想法:

完全揹包問題:即為假設可選擇的物品為無限個,在數學本質上是組合問題。

在本例中,需要求取滿足sum=amount的不重複組合數量。

顯然,最先容易想到的是二維揹包方法,即為遍歷coins陣列,選擇當前所有可能的硬幣數量。

定義dp[coins.size()][amount],得出狀態轉移方程。

在這種情況下,事件複雜度為O(coins.size()*amount^2),空間複雜度為O(coins.size()*amount)

注意到dp過程中的資料傳遞只在[i]和[i+1]之間發生,此處最佳化了空間複雜度,但時間複雜度仍然不變。

這裡我們給出一個示例程式碼:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(int i=0;i<=coins.size();i++) {
            if(i==coins.size()) {
                return dp[amount];
            }

            vector<int> temp(amount+1, 0);
            for(int j=0;j*coins[i]<=amount;j++) {
                int sum = j*coins[i];
                for(int k=amount;k-sum>=0;k--) {
                    temp[k] += dp[k-sum];
                }
            }
            swap(dp, temp);
        }
        return 0;
    }
};

降維

嘗試執行以上程式碼,發現雖然能透過測試,但是耗時高到天際,顯然不是一個好的解決方案。

這裡進入今天的主題,二維dp降階。事實上在上文程式碼中已經完成了空間層面的降階,只需要考慮時間層面。

我們模擬其中一次轉移的程式碼,進入迴圈for(int i=0;i<=coins.size();i++) {...}

假設此時amount = 4,coins[i] = 2

dp初始狀態為:

此時剛進入迴圈,vector temp暫時為空(全0):

第1輪,選擇coin number = 0,sum=0,temp[k] += dp[k-0]; 即為將dp中內容複製到temp中

第2輪,選擇coin number = 1,sum=1*2=2,temp[k] += dp[k-2];

第3輪,選擇coin number = 2,sum=2*2=4,temp[k] += dp[k-4];

第4輪,coin number = 3,sum=3*2=6, 6>4,退出本輪迴圈

由以上圖可以看出,迴圈中每一次相加就相當於對整體陣列做了一次向上平移,offset=2。

這裡我們想要在一個迴圈中完成上述的所有工作,可以觀察到如下公式:

temp[0] = dp[0]

temp[2] = dp[2] + dp[0] = dp[2] + temp[0]

temp[4] = dp[4] + dp[2] + dp[0] = dp[4] + temp[2]

......

那麼我們可以考慮下標從小到大的累加,這樣,較大的下標相加的時候就自動處理了前面的部分,在演算法上這是一種字首和(prefix)思想。

這樣,我們有如下程式碼:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(int i=0;i<coins.size();i++) {
            for(int j=coins[i];j<=amount;j++) {
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

此時的時間複雜度為O(coins.size()*amount),空間複雜度為O(amount)

相關文章