一,问题描述
给定一组硬币数,找出一组最少的硬币数,来找换零钱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,这显然是无法做到的嘛(除非是强盗:) )
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); } }