硬币找零问题的动态规划实现

一,问题描述

给定一组硬币数,找出一组最少的硬币数,来找换零钱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

posted @ 2016-06-12 21:38  大熊猫同学  阅读(23458)  评论(1编辑  收藏  举报