動態規劃5:找零錢問題

LingLee_荊棘鳥發表於2017-07-18

題目:

有陣列penny,penny中所有的值都為正數且不重複。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim(小於等於1000)代表要找的錢數,求換錢有多少種方法。給定陣列penny及它的大小(小於等於50),同時給定一個整數aim,請返回有多少種方法可以湊成aim。

樣例:[1,2,4],3,3  返回:2(即1 1 1,1 2)


思路:

表[n][aim+1]記錄子問題結果,dp[i][j]表示錢為j,且貨幣為penny[0...i]種時,共有的找錢方式。


目標值從aim=0開始


將這個問題分解成為N行,aim+1列的矩陣dp[n][aim+1],矩陣中每個值dp[i][j]:arr[0~i]來拼湊出目標值j的方案數目,於是將複雜問題轉化成為了N*(aim+1)個子問題。

初始條件,可知dp[i][0]=1,對於第一行dp[0][j],表示只用arr[0]這種貨幣進行拼湊,那麼只有該貨幣的整數倍的錢數才能拼湊成功。所以dp[0][j]=1(j是arr[0]的倍數),否則dp[0][j]=0。

對於任意的一個值dp[i][j]表示使用arr[0~i]的元素來拼湊出目標值為j的方案數目,總結規律可以發現:dp[i][j]=dp[i-1][j]+dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]+……arr[i]選0,1,2,...個時的情況

對動態規劃矩陣中的計算順序路徑作出規定,只有按行從上到下,每行從左到右進行計算,才能保證在計算dp[i][j]時dp[i-1][j]+dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]+……都已經計算出來,否則就無法解決問題。因此在本問題中,在計算出了第一行第一列的結果dp[i][j]之後,對之後的dp[i][j]不能隨意計算,必須按照從第1行第2列開始逐行逐列地進行計算,可以使用一個雙層遍歷來進行計算所有值。當所有dp[i][j]計算完成後,矩陣右下角的元素值dp[n-1][aim]就是所求的結果值。

總結:

對於任意一個dp[i][j]在求解的過程中,需要對i-1行中的若干個結點值進行求和運算,即要通過一個迴圈函式按照for(int k=0,k*arr[i]<j,k++)對該列進行遍歷和列舉,於是對於每個結點的列舉的時間複雜度是O(aim),由於要對n*(aim+1)個結點進行列舉,因此總的時間複雜度是O(n*aim^2)

 

例如對於上面dp[i][j]=dp[i-1][j]+dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]+……的計算過程可以進行進一步的簡化,分析發現,例如圖中所示,dp[i-1][j-arr[i]]+dp[i-1][j-arr[i]*2]的計算結果就是dp[i][j-arr[i]]的計算結果,於是dp[i][j]= dp[i-1][j]+ dp[i][j-arr[i]],同時由於這2項都出現在dp[i][j]的計算順序之前,因此可以直接拿來使用,因此對於每一項dp[i][j]在計算時省去了遍歷列舉的過程,於是每一個dp[i][j]的計算時間複雜度降低為O(1),因此總的時間複雜度降低為O(n*aim)。這個規律是因為計算時嚴格按照逐行逐列路徑進行計算時才有的,因此使用動態規劃時按照固定路徑進行計算可以為進一步的優化帶來可能。

public class Exchange {
    public int countWays(int[] penny, int n, int aim) {
        // write code here
        int[][] dp=new int[n][aim+1];
        //初始狀態
        for(int i=0;i<n;i++) dp[i][0]=1;//aim=0時
        for(int j=1;j<=aim;j++){//第一行
            int i=penny[0];
            if(j%i==0) dp[0][j]=1;
            else dp[0][j]=0;
        }
        
        //從上到下 從左到右
        for(int i=1;i<n;i++){
            for(int j=1;j<=aim;j++){
                if(j>=penny[i]){//在前i-1項裡面拼湊j,和在前i項裡拼湊j-penny[i]--預設已經選擇一個i
                    dp[i][j]=dp[i-1][j]+dp[i][j-penny[i]];
                }else{
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
        
        return dp[n-1][aim];
    }
}

用一維陣列:每一列看成一維

public class Exchange {
    public int countWays(int[] penny, int n, int aim) {
        // write code here
        int[] dp=new int[aim+1];//表示每種錢數j有多少種拼湊情況
        
        for(int j=0;j<=aim;j++){//初始
            if(j%penny[0]==0) dp[j]=1;
            else dp[j]=0;
        }
        
        for(int i=1;i<n;i++){
            for(int j=1;j<=aim;j++){
                if(j>=penny[i]) dp[j]=dp[j-penny[i]]+dp[j];
            }
        }
        return dp[aim];
    }
}



相關文章