一,問題描述
給定一組硬幣數,找出一組最少的硬幣數,來找換零錢N。
比如,可用來找零的硬幣為: 1、3、4 待找的錢數為 6。用兩個面值為3的硬幣找零,最少硬幣數為2。而不是 4,1,1
因此,總結下該問題的特征:①硬幣可重復多次使用。②在某些情況下,該問題可用貪心算法求解。具體可參考:某種 找換硬幣問題的貪心算法的正確性證明
二,動態規划分析
為了更好的分析,先對該問題進行具體的定義:將用來找零的硬幣的面值存儲在一個數組中。如下:
coinsValues[i] 表示第 i 枚硬幣的面值。比如,
第 i 枚硬幣 面值
1 1
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