動態規劃之硬幣表示問題

問題描述:java

  有數量不限的硬幣,幣值爲25分、10分、5分和1分,請編寫代碼計算n分有幾種表示法。算法

求解思路:數組

  這也是典型的動態規劃問題,咱們能夠這樣考慮:當只有1分的硬幣時,n從1到n分別有多少種表示方法;當有1分和5分的硬幣時,n從1到n分別有多少種表示方法,所以類推,直到咱們將1分、5分、10分和25分的硬幣所有使用完。思想相似於0-1揹包問題,0-1揹包問題的具體求解方法能夠參考個人上一篇博客動態規劃之0-1揹包問題。咱們用數組coins[i]={1,5,10,25}表示各類幣值,此時能夠維護一張二維表ways[i][j],其中橫座標表示前i種表示幣值,j表示硬幣的總值,則ways[i][j]表示能用前i種硬幣來表示j分的方法數。app

 當增長一種新的硬幣幣值時,有兩種狀況:測試

(1)不加入此種幣值:ways[i][j]=ways[i-1][j];spa

(2)加入此種幣值:加入該枚硬幣以前的方法數爲ways[i][j-coins[i]],那麼加入該枚硬幣以後構成j分的方法數也爲ways[i][j-coins[i]]。.net

 所以當增長一種新的幣值時,j分的表示方法數爲ways[i][j]=ways[i-1][j]+ways[i][j-coins[i]]。code

代碼實現blog

[java] view plain copy遞歸

 

  1. import java.util.Scanner;  
  2. public class Main {  
  3.     public static void main(String[] args) {  
  4.         Scanner sc = new Scanner(System.in);  
  5.         int n = sc.nextInt();  
  6.         int[] coins = {1, 5, 10, 25};  
  7.         int[][] ways = new int[4][n + 1];  
  8.         for (int i = 0; i < 4; i++)  
  9.             ways[i][0] = 1; //第0行初始化爲1  
  10.         for (int j = 1; j <= n; j++)  
  11.             ways[0][j] = 1; //第0列初始化爲1  
  12.         for (int i = 1; i < 4; i++) {  
  13.             for (int j = 1; j <= n; j++) {  
  14.                 if (j >= coins[i])  
  15.                     ways[i][j] = ways[i - 1][j] + ways[i][j - coins[i]];  
  16.                 else  
  17.                     ways[i][j] = ways[i - 1][j];  
  18.             }  
  19.         }  
  20.         System.out.println(ways[3][n]);  
  21.     }  
  22. }  

固然,維護二維表未免過於複雜,咱們能夠維護一張一維表,即用一維數組ways[j]來記錄j分的表示方法數。改進的代碼實現以下:

[java] view plain copy

 

  1. import java.util.Scanner;  
  2. public class Main {  
  3.     public static void main(String[] args) {  
  4.         Scanner sc=new Scanner(System.in);  
  5.         int n=sc.nextInt();  
  6.         int []coins={1,5,10,25};  
  7.         int []ways=new int[n+1];  
  8.         ways[0]=1;  
  9.         for(int i=0;i<4;i++){  
  10.             for(int j=coins[i];j<=n;j++){  
  11.                 ways[j]=ways[j]+ways[j-coins[i]];  
  12.             }  
  13.         }  
  14.         System.out.println(ways[n]);  
  15.     }  
  16. }

 

問題:要找K元的零錢,零錢的種類已知,保存在數組coins[]中,要求:求出構成K所需的最少硬幣的數量和零錢的具體數值。
分析:(1)貪心算法:,先從面額最大的硬幣開始嘗試,一直往下找,知道硬幣總和爲N。可是貪心算法不能保證可以找出解(例如,給,2,3,5,而後N=11,致使無解5,5,1)。
(2)動態規劃:
思想:快速實現遞歸(將前面計算的結果保存在數據裏,後面重複用的時候直接調用就行,減小重複運算)
動態規劃算法與分治法相似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,而後從這些子問題的解獲得原問題的解。與分治法不一樣的是,適合 於用動態規劃求解的問題,經分解獲得子問題每每不是互相獨立的。若用分治法來解這類問題,則分解獲得的子問題數目太多,有些子問題被重複計算了不少次。如 果咱們可以保存已解決的子問題的答案,而在須要時再找出已求得的答案,這樣就能夠避免大量的重複計算,節省時間。咱們能夠用一個表來記錄全部已解的子問題 的答案。無論該子問題之後是否被用到,只要它被計算過,就將其結果填入表中。
代碼:

/*找零錢問題:要找 K元的零錢,零錢的種類有 coins[],要求零錢的張數最少,用 road[]來找出具體使用的零錢*/
public class MinCount_coins {
    public static void main (String[] args) { 
       int coins[]={3,5};
       int k=4;
       int road[]=new int[k+1];
       int min=getMinCount(k ,coins ,road );
       if(min>Integer. MAX_VALUE-k ){ //min 沒有另外賦值,則表示零錢不夠
        System.out. println( "零錢不夠!" );
       }else{
        System.out. println( "數值爲" +k +" 時,須要的最少的硬幣數爲: "+ min);
           for(int j=k;j>0;){
            System.out. print( road[ j]+ "\t");
            j=j-road[j];  //j爲當前要找的零錢值, road[j]爲當前面額下,最近加入的零錢
           }
       }
    } 

    public static int getMinCount (int k,int c[],int r[]){
     int a[]=new int[k+1];//保存最近加入的零錢值
     a [0]=0;
     for(int x=1;x<k+1;x++){ //要求a[k],先求a[1]~a[k-1]
         if(x>=c[0]){  //給a[x]附初值
             a[x]=a[x-c[0]]+1;
             r[x]=c[0];
         }else{   //要找零錢比最小零錢值還小,零錢不夠
             a[x]=Integer.MAX_VALUE- k;
         }
         for(int i=1;i<c.length;i++){
             if(x>=c[i]&&(a[x]>a[x-c[i]]+1)){//x-c[i]表示當前值減去coins[]中值,便可由前面那些子問題+1一次得來
                  a[ x]= a[ x- c[ i]]+1;
                  r[ x]= c[ i];
             }
         }
     }
     return a[k];
    }
}

 

本文中的解解法綜合考慮了各類狀況,好比改變了零錢的種類,零錢不夠等狀況

 

 

動態規劃的基本思想是將待求解問題分解成若干個子問題,先求解子問題,並將這些子問題的解保存起來,若是之後在求解較大子問題的時候須要用到這些子問題的解,就能夠直接取出這些已經計算過的解而免去重複運算。保存子問題的解可使用填表方式,例如保存在數組中。

用一個實際例子來體現動態規劃的算法思想——硬幣找零問題。

硬幣找零問題描述:現存在一堆面值爲 V一、V二、V3 … 個單位的硬幣,問最少須要多少個硬幣才能找出總值爲 T 個單位的零錢?假設這一堆面值分別爲 一、二、五、2一、25 元,須要找出總值 T 爲 63 元的零錢。

很明顯,只要拿出 3 個 21 元的硬幣就湊夠了 63 元了。

基於上述動態規劃的思想,咱們能夠從 1 元開始計算出最少須要幾個硬幣,而後再求 2 元、3元…每一次求得的結果都保存在一個數組中,之後須要用到時則直接取出便可。那麼咱們何時須要這些子問題的解呢?如何體現出由子問題的解獲得較大問題的解呢?

其實,在咱們從 1 元開始依次找零時,能夠嘗試一下當前要找零的面值(這裏指 1 元)是否可以被分解成另外一個已求解的面值的找零須要的硬幣個數再加上這一堆硬幣中的某個面值之和,若是這樣分解以後最終的硬幣數是最少的,那麼問題就獲得答案了。

單是上面的文字描述太抽象,先假定如下變量:

values[] : 保存每一種硬幣的幣值的數組
valueKinds :幣值不一樣的硬幣種類數量,即values[]數組的大小

money : 須要找零的面值
coinsUsed[] : 保存面值爲 i 的紙幣找零所需的最小硬幣數

算法描述:

當求解總面值爲 i 的找零最少硬幣數 coinsUsed[ i ] 時,將其分解成求解 coinsUsed[ i – cents]和一個面值爲 cents 元的硬幣,因爲 i – cents < i , 其解 coinsUsed[ i – cents] 已經存在,若是面值爲 cents 的硬幣知足題意,那麼最終解 coinsUsed[ i ] 則等於 coinsUsed[ i – cents] 再加上 1(即面值爲 cents)的這一個硬幣。

下面用代碼實現並測試一下:

 
  1. public class CoinsChange {  
  2.     /**  
  3.      * 硬幣找零:動態規劃算法  
  4.      *   
  5.      * @param values  
  6.      *            :保存每一種硬幣的幣值的數組  
  7.      * @param valueKinds  
  8.      *            :幣值不一樣的硬幣種類數量,即coinValue[]數組的大小  
  9.      * @param money  
  10.      *            :須要找零的面值  
  11.      * @param coinsUsed  
  12.      *            :保存面值爲i的紙幣找零所需的最小硬幣數  
  13.      */ 
  14.     public static void makeChange(int[] values, int valueKinds, int money,  
  15.             int[] coinsUsed) {  
 
    • coinsUsed[0] = 0;  
    •         // 對每一分錢都找零,即保存子問題的解以備用,即填表  
    •         for (int cents = 1; cents <= money; cents++) {  
    •  
    •             // 當用最小幣值的硬幣找零時,所需硬幣數量最多  
    •             int minCoins = cents;  
    •  
    •             // 遍歷每一種面值的硬幣,看是否可做爲找零的其中之一  
    •             for (int kind = 0; kind < valueKinds; kind++) {               
    •                 // 若當前面值的硬幣小於當前的cents則分解問題並查表  
    •                 if (values[kind] <= cents) {  
    •                     int temp = coinsUsed[cents - values[kind]] + 1;  
    •                     if (temp < minCoins) {  
    •                         minCoins = temp;  
    •                     }  
    •                 }  
    •             }  
    •             // 保存最小硬幣數  
    •             coinsUsed[cents] = minCoins;  
    •  
    •             System.out.println("面值爲 " + (cents) + " 的最小硬幣數 : " 
    •                     + coinsUsed[cents]);  
    •         }  
    •     }  
    •       
    •     public static void main(String[] args) {  
    •  
    •         // 硬幣面值預先已經按降序排列  
    •         int[] coinValue = new int[] { 25211051 };  
    •         // 須要找零的面值  
    •         int money = 63;  
    •  
    •  // 保存每個面值找零所需的最小硬幣數,0號單元捨棄不用,因此要多加1  
    •         int[] coinsUsed = new int[money + 1];  
    •  
    •         makeChange(coinValue, coinValue.length, money, coinsUsed);  
    •     }  
    • 測試結果:

      面值爲 1 的最小硬幣數 : 1
      面值爲 2 的最小硬幣數 : 2
      面值爲 3 的最小硬幣數 : 3
      面值爲 4 的最小硬幣數 : 4
      面值爲 5 的最小硬幣數 : 1
      面值爲 6 的最小硬幣數 : 2
      ...
      ...
      面值爲 60 的最小硬幣數 : 3
      面值爲 61 的最小硬幣數 : 4
      面值爲 62 的最小硬幣數 : 4
      面值爲 63 的最小硬幣數 : 3

       上面的代碼並無給出具體應該是哪幾個面值的硬幣,這個能夠再使用一些數組保存而打印出來。

/**
 * 功能:給定數量不限的硬幣,幣值爲25分,10分,5分,1分,計算n分有幾種表示法。

 */

 

 

[java] view plain copy

 

  1. public static int makeChange(int n){  
  2.     return makeChange(n,25);  
  3. }  
  4.   
  5. /** 
  6.  * 遞歸的終止條件:徹底簡化爲1分。 
  7.  * @param n 
  8.  * @param denom 
  9.  * @return 
  10.  */  
  11. public static int makeChange(int n,int denom){  
  12.     int next_denom=0;  
  13.     switch(denom){  
  14.     case 25:  
  15.         next_denom=10;  
  16.         break;  
  17.     case 10:  
  18.         next_denom=5;  
  19.         break;  
  20.     case 5:  
  21.         next_denom=1;  
  22.         break;  
  23.     case 1:  
  24.         return 1;  
  25.     }  
  26.       
  27.     int ways=0;  
  28.     for(int i=0;i*denom<=n;i++){  
  29.         ways+=makeChange(n-i*denom,next_denom);  
  30.     }  
  31.     return ways;  
相關文章
相關標籤/搜索