1. 程式人生 > >動態規劃-硬幣找零問題四種情況

動態規劃-硬幣找零問題四種情況

題目1:給定陣列arr,arr中所有的值都是正數且不重複。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim代表要找的錢數,求組成aim的最少貨幣數。

舉例: 

arr[5,2,3],aim=20。  4張5元可以組成20元,其他的找錢方案都要使用更多張的貨幣,所以返回4。

題解:

一眼看去這道題好像可以用貪心演算法可解,但是仔細分析發現有些值是不可以的,例如arr[1,3,4],aim=6 :用貪心演算法算最少錢數為3 (4+1+1),但是我們可以明顯的發現用兩張3元的就夠了,所以用貪心演算法不可解。

其實這是一道經典的動態規劃方法,我們可以構造一個dp陣列,如果arr的長度為N,則dp陣列的行數為N,列數為aim+1,dp[i][j] 的含義是:在可以任意使用arr[0..i]貨幣的情況下,組成j所需要的最小張數。

明白以上定義後我們初始化第一行與第一列,第一行dp[0][0..aim]中每一個元素dp[0][j]表示用arr[0]貨幣找開面額 j所需要的最少貨幣數,此時我們只能選取arr[0]這一張貨幣,所以只有arr[0]的整數倍的面額錢才可以找開,例如當arr[0]=3,aim=10時,只能找開3,6,9的貨幣,而其他面額的則無法找開,所以將arr[0][3,6,9]初始化為1,2,3 除此之外其他值初始化為整形int的最大值INT_MAX表示無法找開。對於第一列dp[0..n][0] 中的每一個元素dp[i][0]表示用arr[i]組成面額為0的錢的最少貨幣數,完全不需要任何貨幣,直接初始化為0即可。

     對於剩下的任意dp[i][j],我們依次從左到右,從上到下計算,dp[i][j]的值可能來自下面:

  • 完全不使用當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j]的值
  • 只使用1張當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j-arr[i]]+1
  • 只使用2張當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j-2*arr[i]]+2
  • 只使用3張當前貨幣arr[i]的情況下的最少張數,即dp[i-1][j-3*arr[i]]+3
  • …..
  • import java.util.*;
    public class Coin3 {
        public static void main(String args[]){
            Scanner in=new Scanner(System.in);
            int arr[]=new int[]{1,3,5};
            while(in.hasNext()){
                int total=in.nextInt();
                int dp[][]=new int[arr.length][total+1];
                //注意初始化,當找零為0時,初始化為1種
                for(int i=0;i<arr.length;i++){
                    dp[i][0]=1;
                }
                for(int j=1;j<=total;j++){
                    if(j%arr[0]==0){
                        dp[0][j]=1;
                    }else{
                        dp[0][j]=0;
                    }
                }
                //注意更新策略,使用在arr[i][j]處使用arr[i]和不使用arr[i]兩種情況之和
                for(int i=1;i<arr.length;i++){
                    for(int j=1;j<=total;j++){
                        if(arr[i]>j){
                            dp[i][j]=dp[i-1][j];
                        }else{
                            dp[i][j]=dp[i-1][j]+dp[i][j-arr[i]];
                        }
                    }
                }
                System.out.println(dp[arr.length-1][total]);
            }
        }
    }
    

以上所有情況中,最終取張數最小的,即dp[i][j] = min( dp[i-1][j-k*arr[i]]+k )( k>=0 )

=>dp[i][j] = min{ dp[i-1][j], min{ dp[i-1][j-x*arr[i]]+x (1<=x) } }    令x = y+1

=>dp[i][j] = min{ dp[i-1][j], min{ dp[i-1][j-arr[i]-y*arr[i]+y+1 (0<=y) ] } } 

又有 min{ dp[i-1][j-arr[i]-y*arr[i]+y (0<=y) ] } => dp[i][ j-arr[i] ] ,所以,最終有:dp[i][j] = min{ dp[i-1][j], dp[i][j-arr[i]]+1 }。如果j-arr[i] < 0,即發生了越界,說明arr[i]太大了,用一張都會超過錢數j,此時dp[i][j] = dp[i-1][j]。

import java.util.*;     //每種型別的貨幣可以使用多次
public class coin1 {
    public static void main(String args[]){
        Scanner in=new Scanner(System.in);
        while(in.hasNext()){
            int arr[]=new int[]{1,3,4,5};
            int total=in.nextInt();
            int dp[][]=new int[arr.length][total+1];
            //初始化邊界(第一列),需要找零為0時,所需要的硬幣數為0
            for(int i=0;i<arr.length;i++)
                dp[i][0]=0;
            //初始化邊界(第一行),只用arr[0]去找零
            for(int j=1;j<=total;j++){
                if(j%arr[0]==0){
                    dp[0][j]=j/arr[0];
                }else {
                    dp[0][j]=1000000;
                }
            }
            //動態更新每一行和每一列,注意理解動態規劃的公式
            for(int i=1;i<arr.length;i++){
                for(int j=1;j<=total;j++){
                    if(j>=arr[i]){
                        dp[i][j]=Math.min(dp[i-1][j],dp[i][j-arr[i]]+1);
                    }else{
                        dp[i][j]=dp[i-1][j];
                    }
                }
            }
            System.out.println(dp[arr.length-1][total]);
        }
    }
}

題目2: 給定陣列arr,arr中所有的值都為正數,每個值僅代表一張錢的面值,再給定一個整數aim代表要找的錢數,求組成aim的最小貨幣數。

題解: 相對於上一題,這道題的arr中的錢只有一張,而不是任意多張,構造dp陣列的含義也同上,但是此時略有不同,

dp第一行dp[0][0..aim]的值表示只使用一張arr[0]貨幣的情況下,找某個錢數的最小張數。比如arr[0]=2,那麼能找開的錢數僅為2, 所以令dp[0][2]=1。因為只有一張錢,所以其他位置所代表的錢數一律找不開,一律設為INT_MAX。第一列dp[0…N-1]表示找的錢數為0時需要的最少張數,錢數為0時完全不需要任何貨幣,所以全設為0即可。

剩下的位置從左到右,從上到下計算,dp[i][j]可能的值來自於以下兩種情況

  1. dp[i][j]的值代表在可以任意使用arr[0..i]貨幣的情況下,組成j所需要的最小張數。可以任意使用arr[0..i]貨幣的情況當然包括不使用arr[i]的貨幣,而只使用任意arr[0..i-1]貨幣的情況,所以dp[i][j]的值可能為dp[i-1][j]。
  2. 因為arr[i]只有一張不能重複使用,所以我們考慮dp[i-1][j-arr[i]]的值,這個值代表在可以任意使用arr[0..i-1]貨幣的情況下,組成j-arr[i]所需的最小張數。從錢數為j-arr[i]到錢數j,只用在加上這張arr[i]即可。所以dp[i][j]的值可能等於do[i-1][j-arr[i]]+1。
  3. 如果dp[i-1][j-arr[i]]中j-arr[i] < 0,也就是位置越界了,說明arr[i]太大了,只用一張就會超過錢數j,令dp[i][j]=dp[i-1][j]即可。
    import java.util.*;
    public class Coin2 {
        public static void main(String args[]){
            Scanner in=new Scanner(System.in);
            while(in.hasNext()){
                int arr[]=new int[]{1,1,3,3,3,4,4,4};
                int total=in.nextInt();
                int dp[][]=new int[arr.length][total+1];
                for(int i=0;i<arr.length;i++)
                    dp[i][0]=0;
                //初始化
                for(int j=0;j<=total;j++){
                    if(j==arr[0]){
                        dp[0][j]=1;
                    }else{
                        dp[0][j]=100010;
                    }
                }
                //更新策略
                for(int i=1;i<arr.length;i++){
                    for(int j=1;j<=total;j++){
                        if(arr[i]>j){
                            dp[i][j]=dp[i-1][j];
                        }else{
                            dp[i][j]=Math.min(dp[i-1][j-arr[i]]+1,dp[i-1][j]);
                        }
                    }
                }
                if(dp[arr.length-1][total]>100000){
                    System.out.println("找不開");
                }else{
                    System.out.println(dp[arr.length-1][total]);
                }
            }
        }
    }