1. 程式人生 > >錢幣組合問題(動態規劃)

錢幣組合問題(動態規劃)

錢幣組合問題:(每種硬幣不限次數)

假設我們有8種不同面值的硬幣{1,2,5,10,20,50,100,200},用這些硬幣組合夠成一個給定的數值n。例如n=200,那麼一種可能的組合方式為200 = 3 * 1 + 1*2 + 1*5 + 2*20 + 1 * 50 + 1 * 100. 問總過有多少種可能的組合方式?

解題思路:

給定一個數值sum,假設我們有m種不同型別的硬幣{V1, V2, ..., Vm},如果要組合成sum,那麼我們有

sum = x1 * V1 + x2 * V2 + ... + xm * Vm

求所有可能的組合數,就是求滿足前面等值的係數{x1, x2, ..., xm}的所有可能個數。

[思路1] 當然我們可以採用暴力列舉,各個係數可能的取值無非是x1 = {0, 1, ..., sum / V1}, x2 = {0, 1, ..., sum/ V2}等等。這對於硬幣種類數較小的題目還是可以應付的,比如華為和創新工廠的題目,但是複雜度也很高O(sum/V1 * sum/V2 * sum/V3 * ...)

[思路2] 從上面的分析中我們也可以這麼考慮,我們希望用m種硬幣構成sum,根據最後一個硬幣Vm的係數的取值為無非有這麼幾種情況,xm分別取{0, 1, 2, ..., sum/Vm},換句話說,上面分析中的等式和下面的幾個等式的聯合是等價的。

sum = x1 * V1 + x2 * V2 + ... + 0 * Vm

sum = x1 * V1 + x2 * V2 + ... + 1 * Vm

sum = x1 * V1 + x2 * V2 + ... + 2 * Vm

...

sum = x1 * V1 + x2 * V2 + ... + K * Vm  

其中K是該xm能取的最大數值K = sum / Vm。可是這又有什麼用呢?不要急,我們先進行如下變數的定義:

dp[i][sum] = 用前i種硬幣構成sum 的所有組合數。

那麼題目的問題實際上就是求dp[m][sum],即用前m種硬幣(所有硬幣)構成sum的所有組合數。在上面的聯合等式中:當xn=0時,有多少種組合呢? 實際上就是前i-1種硬幣組合sum,有dp[i-1][sum]種! xn = 1 時呢,有多少種組合? 實際上是用前i-1種硬幣組合成(sum - Vm)的組合數,有dp[i-1][sum -Vm]種; xn =2呢, dp[i-1][sum - 2 * Vm]種,等等。所有的這些情況加起來就是我們的dp[i][sum]。所以:

dp[i][sum] = dp[i-1][sum - 0*Vm] + dp[i-1][sum - 1*Vm]

+ dp[i-1][sum - 2*Vm] + ... + dp[i-1][sum - K*Vm]; 其中K = sum / Vm

換一種更抽象的數學描述就是:

遞迴公式

通過此公式,我們可以看到問題被一步步縮小,那麼初始情況是什麼呢?如果sum=0,那麼無論有前多少種來組合0,只有一種可能,就是各個係數都等於0;

dp[i][0] = 1   // i = 0, 1, 2, ... , m

如果我們用二位陣列表示dp[i][sum], 我們發現第i行的值全部依賴與i-1行的值,所以我們可以逐行求解該陣列。如果前0種硬幣要組成sum,我們規定為dp[0][sum] = 0. 

思路一:暴力窮舉

每種硬幣最多為N/coins,當n較小時,可以窮舉出,注意本程式需要n>200,否則需要更改if判斷的位置。(時間超時)

public static int cb = 0;
        public static void brute(int[] coins,int n){
            //有多少種幣就有多少個for,此題共有7種
            for(int i =0;i<=n/coins[0];i++){
                for(int j =0;j<=n/coins[1];j++){
                    for(int k =0;k<=n/coins[2];k++){
                        for(int l =0;l<=n/coins[3];l++){
                            for(int m =0;m<=n/coins[4];m++){
                                for(int o =0;o<=n/coins[5];o++){
                                    for(int p =0;p<=n/coins[6];p++){
                                        if((coins[0]*i+coins[1]*j+coins[2]*k+coins[3]*l+coins[4]*m+coins[5]*o+coins[6]*p)==n){
                                            cb++;
                                            //System.out.println("1 2 5 10 20 50 100 各使用"+i+j+k+l+m+o+p+"種");
                                        };
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

思路二:分支限界,深度遍歷

去掉大於N的情況,注意每種硬幣允許使用多次,且122與212、221屬於同一種情況,所以限定每次使用的幣種(coins[i])應大於等於上次使用的幣種(coins[coinlocation]) (時間超時)

        static int cc = 0;    //統計總共的數目
        static int[] sum = {0,0,0,0,0,0,0};//用於列印使用狀況
        /**
         * @param coins 錢幣列表
         * @param n 總金額
         * @param count 當前金額
         * @param coinlocation 當前使用幣種的下標
         */
        public static void dfs(int[] coins,int n,int count,int coinlocation){
            if(count==n){
                cc++;
                for(int i =0;i<sum.length;i++){
                    System.out.print("使用 "+coins[i]+"元:"+sum[i]+"個,");
                }
                System.out.println();
            }else if(count > n){
                return;
            }else {
                //遍歷每一種硬幣,允許1 2 2,不允許2,1,2
                for(int i=coinlocation;i<coins.length;i++){
                    sum[i]++;
                    dfs(coins,n,count+coins[i],i);
                    sum[i]--;
                }
            }
        }

思路三:動態規劃

設d[i][sum]為使用第i中錢幣時達到總金額sum的方案數目所以對於錢幣i(coins[i]),可以不使用i、使用一個i、使用兩個i......使用sum/coins[i]個i,即個,其中用d[i][0] = 1(只有一種方案,即什麼幣種也不用)d[0][i] =0(不用任何幣種組成任意錢數,沒有這種操作)。

      public static void dynamic_prommgram(int[] coins,int n){
            int[][] d = new int[coins.length+1][n+1];//length+1表示不適用任何幣種、只使用1、只使用1 2 只使用1 2 3......等等,共length+1種情況,且n+1表示總計0、1.....至n元共n+1種情況
            for(int i = 0;i<=coins.length;i++) d[i][0] = 1;
            for(int i = 1 ;i<=coins.length;i++){//因為d[0][i]是0,所以i從1開始
                for(int sum = 1;sum<=n;sum++){//由於d[i][0]==1,所以j從1開始
                    for(int k=0;k<=sum/coins[i-1];k++){//例如,使用面值為1時,對應的coins[]下標是i-1,邏輯上河實際上不是一致的
                        d[i][sum] +=d[i-1][sum-k*coins[i-1]];
                    }                
                }
            }
            System.out.println(d[coins.length][n]);
        }

當然,程式可以簡化為一維陣列,二維陣列的方式易於理解,但一維陣列的方式更為簡潔,因為d[i][sum]=ξ’d[i-1][X’]=ξ’ξ’d[i-2][X’’]=...即d[i-1]就是累加求和的中間量,所以可以直接使用一維陣列表示:

        public static void main(String[] args){
            long[] d= new long[201];
            d[0]=1;
            int[] v={1,2,5,10,20,50,100
            };
            for(int i=0;i<7;i++){
                for(int j=v[i];j<201;j++){
                    d[j]+=d[j-v[i]];       //相當於                         
                }
            }     
            System.out.println(200+"的組合方式有"+d[200]+"種");
    }