一,問題描述html
給定一組硬幣數,找出一組最少的硬幣數,來找換零錢N。java
好比,可用來找零的硬幣爲: 一、三、4 待找的錢數爲 6。用兩個面值爲3的硬幣找零,最少硬幣數爲2。而不是 4,1,1算法
所以,總結下該問題的特徵:①硬幣可重複屢次使用。②在某些狀況下,該問題可用貪心算法求解。具體可參考:某種 找換硬幣問題的貪心算法的正確性證實數組
二,動態規劃分析post
爲了更好的分析,先對該問題進行具體的定義:將用來找零的硬幣的面值存儲在一個數組中。以下:spa
coinsValues[i] 表示第 i 枚硬幣的面值。好比,code
第 i 枚硬幣 面值htm
1 1blog
2 3排序
3 4
待找零的錢數爲 n (上面示例中 n=6)
爲了使問題總有解,通常第1枚硬幣的面值爲1
考慮該問題的最優子結構:設 c[i,j]表示 可用第 0,1,.... i 枚硬幣 對 金額爲 j 的錢 進行找錢 所須要的最少硬幣數。
i 表示可用的硬幣種類數, j 表示 須要找回的零錢
第 i 枚硬幣有兩種選擇:用它來找零 和 不用它找零。所以,c[i,j]的最優解以下:
c[i,j]= min{c[i-1,j] , c[i, j-coinsValues[i]] + 1} 其中,
c[i-1,j] 表示 不使用第 i 枚硬幣找零時,對金額爲 j 進行找錢所須要的最少硬幣數
c[i, j-coinsValues[i]] + 1 表示 使用 第 i 枚硬幣找零時,對金額爲 j 進行找錢所須要的最少硬幣數。因爲用了第 i 枚硬幣,故使用的硬幣數量要增1
c[i,j] 取兩者的較小值的那一個。
另外,對特殊狀況分析(特殊狀況1)一下:
c[0][j]=Integer.MAXVALUE ,由於 對 金額爲 j 的錢找零,可是可用的硬幣面值 種類爲0,這顯然是沒法作到的嘛(除非是強盜:) )
其實這是一個」未定義「的狀態。它之因此初始爲Integer.MAXVALUE,與《揹包九講》中的」當要求揹包必須裝滿的條件下,價值儘量大「時 的初始化方式同樣。
c[i][0]=0,由於,對 金額爲0的錢 找零,可用來找零的硬幣種類有 i 種,金額爲0啊,怎麼找啊,找個鴨蛋啊。
其實,邊界條件是根據具體的問題而定的。DP太博大了,理解的不深。
另外,還有一種特殊狀況(特殊狀況2),就是: coinsValues[i] > j
這說明第 i 枚硬幣的面值大於 金額 j ,這也不能用 第 i 枚硬幣來找零啊,(欠你5塊錢,可是找你一張百元大鈔)否則就虧了了(吃虧是福啊^~^...or 殺雞焉用牛刀!!!)
有了這個特徵轉移方程:c[i,j]= min{c[i-1,j] , c[i, j-coinsValues[i]] + 1} ,就好寫代碼了。
三,JAVA代碼實現
1 /** 2 * 3 * @param coinsValues 可用來找零的硬幣 coinsValues.length是硬幣的種類 4 * @param n 待找的零錢 5 * @return 最少硬幣數目 6 */ 7 public static int charge(int[] coinsValues, int n){ 8 int[][] c = new int[coinsValues.length + 1][n + 1]; 9 10 //特殊狀況1 11 for(int i = 0; i <= coinsValues.length; i++) 12 c[i][0] = 0; 13 for(int i = 0; i <= n; i++) 14 c[0][i] = Integer.MAX_VALUE; 15 16 for(int j_money = 1; j_money <=n; j_money++) 17 { 18 19 for(int i_coinKinds = 1; i_coinKinds <= coinsValues.length; i_coinKinds++) 20 { 21 if(j_money < coinsValues[i_coinKinds-1])//特殊狀況2,coinsValues數組下標是從0開始的, c[][]數組下標是從1開始計算的 22 { 23 c[i_coinKinds][j_money] = c[i_coinKinds - 1][j_money];//只能使用 第 1...(i-1)枚中的硬幣 24 continue; 25 } 26 27 //每一個問題的選擇數目---選其中較小的 28 if(c[i_coinKinds - 1][j_money] < (c[i_coinKinds][j_money - coinsValues[i_coinKinds-1]] +1)) 29 c[i_coinKinds][j_money] = c[i_coinKinds - 1][j_money]; 30 else 31 c[i_coinKinds][j_money] = c[i_coinKinds][j_money - coinsValues[i_coinKinds-1]] +1; 32 } 33 } 34 return c[coinsValues.length][n]; 35 }
①第28行-20行 就是狀態轉換方程的表示。
②第16行-第19行的for循環體現就是動態規劃的自底向上的思想。
複雜度分析:從代碼19-20行的for循環來看,時間複雜度爲O(MN),M爲可用的硬幣種類數目,N爲待找的零錢金額
從理論上分析,DP(Dynamic Programming)的時間複雜度爲子問題的個數乘以每一個子問題的可用選擇數。顯然,這個有MN個子問題,每一個子問題有兩種選擇(選第i枚硬幣和不選第i枚硬幣)。
一直很好奇DP經過列一個方程就把一個問題給解決了,其實從16-19行的for循環來看,循環的下標是由小到大,說明它先解決子問題,而後再把原問題給解決了。
四,參考資料
http://haolloyin.blog.51cto.com/1177454/352115
五,完整代碼
import java.util.Arrays; public class DPCharge { /** * * @param coinsValues 可用來找零的硬幣 coinsValues.length是硬幣的種類 * @param n 待找的零錢 * @return */ public static int charge(int[] coinsValues, int n){ int[][] c = new int[coinsValues.length + 1][n + 1]; //特殊狀況1 for(int i = 0; i <= coinsValues.length; i++) c[i][0] = 0; for(int i = 0; i <= n; i++) c[0][i] = Integer.MAX_VALUE; for(int j_money = 1; j_money <=n; j_money++) { for(int i_coinKinds = 1; i_coinKinds <= coinsValues.length; i_coinKinds++) { if(j_money < coinsValues[i_coinKinds-1])//特殊狀況2 { c[i_coinKinds][j_money] = c[i_coinKinds - 1][j_money]; continue; } //每一個問題的選擇數目---選其中較小的 if(c[i_coinKinds - 1][j_money] < (c[i_coinKinds][j_money - coinsValues[i_coinKinds-1]] +1)) c[i_coinKinds][j_money] = c[i_coinKinds - 1][j_money]; else c[i_coinKinds][j_money] = c[i_coinKinds][j_money - coinsValues[i_coinKinds-1]] +1; } } return c[coinsValues.length][n]; } public static void main(String[] args) { int[] coinsValues = {1,3,4}; Arrays.sort(coinsValues);//須要對數組排序,否則會越界..... int n = 6; int minCoinsNumber = charge(coinsValues, n); System.out.println(minCoinsNumber); } }
原文:http://www.cnblogs.com/hapjin/p/5578852.html