动态规划算法思想解决找零钱问题

来源:互联网 发布:网络攻击监测网站 编辑:程序博客网 时间:2024/05/17 01:52

动态规划算法思想解决找零钱问题

前言

    关于找零钱问题,网上已经有很多相关的资料以及优秀的文章博客等。这里写这篇博客的初衷很简单,就是为了方便自己,回过头来捡起这个知识能快一点,接受起来更易理解点;他人的文章写的再好,毕竟是别人的,学习起来总有一定的困难。想法上,理解上总有一些不同的地方。所以在解决这个问题后,记录下我的笔记。

一、动态规划(DP)

    1.1 概念

    动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法

说明:上面是对该术语的简单解释,不是算法分析与设计中对动态规划的定义。本人对算法的认识比较浅显解释这个还是很困难的,而书本上概念性的知识我认为并不适合去学习理解。当然关于动态规划定义在1.5动态规划的理解中有详细介绍。

    1.2 性质

    动态规划一般用来处理最优解的问题。使用动态规划算法思想解决的问题一般具有最优子结构性质和重叠子问题这两个因素。

<1> 最优子结构

     一个问题的最优解包含其子问题的最优解,这个性质被称为最优子结构性质 

<2> 重叠子问题

     递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。这种性质称为子问题的重叠性质。

    1.3 基本步骤

<1> 找出最优解的性质,并刻划其结构特征。

<2> 递归地定义最优值。

<3> 以自底向上的方式计算出最优值。

<4> 根据计算最优值时得到的信息,构造最优解。

    1.4 动态规划与分治法

    动态规划和分治法有相似之处,都是将待解决问题分解为若干子问题。不同之处,分治法求解时有些子问题被重复计算了许多次;动态规划实现了存储这些子问题的解,以备子问题重复出现,当重叠子问题出现,找到已解决子问题的解即可,避免了大量的重复计算。

    1.5 动态规划的理解(来源:知乎)

    这里引用知乎王勐对动态规划的理解来作为动态规划的引入。


    动态规划是对于 某一类问题 的解决方法!!重点在于如何鉴定“某一类问题”是动态规划可解的而不是纠结解决方法是递归还是递推!

    当你企图使用计算机解决一个问题是,其实就是在思考如何将这个问题表达成状态(用哪些变量存储哪些数据)以及如何在状态中转移(怎样根据一些变量计算出另一些变量)。所以所谓的空间复杂度就是为了支持你的计算所必需存储的状态最多有多少,所谓时间复杂度就是从初始状态到达最终状态中间需要多少步!

一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的!每个阶段只有一个状态->递推;
    每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
    每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
    每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到
    这个性质叫做最优子结构;
而不管之前这个状态是如何得到的










二、找零钱问题

    2.1 问题描述

    在现实生活中,经常遇到找零问题,假设有数目不限的面值为1元,5角,1角的硬币。给出需要找零金额,求出找零方案,要求:使用的硬币数目最少。

找零钱问题:

    假设只有 1 分、 2 分、五分、 1 角、二角、 五角、 1 元的硬币。在超市结账时,如果需要找零钱,收银员希望将最少的硬币数找给顾客。那么,给定需要找的零钱数目,如何求得最少的硬币数呢?

    2.2 问题分析


<1> 将硬币种类封装到数组 int coinsValues[]中,此处即
    int[] coinsValues = {1,2,5,10,20,50,100};
coinsValues[i] 表示第 i 枚硬币的面值为 coinsValues[i] ,单位:分。
coinsValues.length 表示硬币的种类

说明

    为了使问题总有解,一般有一枚面值为 1 的硬币,此处已有。

<2> 设 

    chargeOptimalSolution[coinKind][money]

表示可用第0、1、2...coinKind 种硬币对找零金额 money 找零

时所需要的最少硬币数。即当前问题的最优解。

(1) coinKind 硬币种类,用来表示第几种硬币

(2) money 当前找零总金额

<3> 对于某一种硬币来说,我们可以使用该种硬币进行找零,也可以不使用该种硬币进行找零。

于是根据当前逻辑分析,写出下面两个方法

    chargeOptimalSolution[coinKind - 1][money]    chargeOptimalSolution[coinKind][money - coinsValues[coinKind - 1]] + 1

(1) chargeOptimalSolution[coinKind - 1][money]

表示使用第 coinKind 种硬币的情况下找零所需的最少硬币数

(2) chargeOptimalSolution[coinKind][money - coinsValues[coinKind]] + 1

表示不使用第 coinKind 种硬币的情况下找零所需的最少硬币数,因为使用了该种硬币一次,最少硬币数 chargeOptimalSolution[][] 加一。

<4> 最优解则为二者情况中较小的一个
    //使用第 i(coinkind) 种硬币时所需的最小硬币数-- 递推 --    //不使用第 i(coinkind) 种硬币找零时需要的最小硬币数-- 递推 --    int numberByCoinKind = chargeOptimalSolution[coinKind - 1][money];    int numberNotByCoinKind = chargeOptimalSolution[coinKind][money - coinsValues[coinKind-1]] + 1;    //逻辑判断硬币数目选其中较小的    chargeOptimalSolution[coinKind][money] =         numberByCoinKind < numberNotByCoinKind ? numberByCoinKind : numberNotByCoinKind;

<5> 特殊情况

    以上业务逻辑,基本上就解决了这个问题。下面说说特殊情况。

(1)边界问题:

不存在对金额不为 0 的情况下找零的硬币种类为 0 ;这不就是摆明了不想找钱给顾客,黑店啊。

不存在对币种不为 0 的情况下对找零金额为 0 的进行找零;这不就是摆明了送钱吗,赔本生意。

对找零金额 money (money != 0),可用硬币种类为 0 种,不找钱了?

    chargeOptimalSolution[0][money]

对找零金额为 0,可用硬币coinKind (coinKind != 0),有钱任性,送钱了,顾客一脸茫然。

    chargeOptimalSolution[coinKind][0]

(2)可用币种面值大于找零金额的情况

收银员收费后需要找零 5 角,找了一张面值 50 元的面币,还干不干了这生意。

    coinsValues[coinKind] > money

<6>综上
    找零问题的解决点就在下面这个公式:
    最优解(最少硬币数) = min{chargeOptimalSolution[coinKind - 1][money], chargeOptimalSolution[coinKind][money - coinsValues[coinKind]] + 1}

min{a, b} 表示 a, b 中最小的那个数。


    2.3 代码实现

    ChargeProblem.Java

package common.test;import java.util.Arrays;/** *  * @since 2017-10-16 * @author niaonao * */public class ChargeProblem {    /** * 通过面值为 coinsValues[i] 的硬币对金额 chargeMoney 找零 * @param coinsValues 硬币面值coinsValues[i],硬币面值种类数量coinsValues.length * @param chargeMoney 找零金额 * @return 最小找零硬币数目 */    public static int charge(int[] coinsValues, int chargeMoney){    int coinsKinds = coinsValues.length;        int[][] chargeOptimalSolution = new int[coinsKinds + 1][chargeMoney + 1];                //当找零金额为 0 时,不需要找零,最少找零硬币数量为 0        for(int i = 0; i <= coinsKinds; i++)        chargeOptimalSolution[i][0] = 0;                //当找零金额不为 0 时,找零硬币种类不可为 0         for(int i = 0; i <= chargeMoney; i++)        chargeOptimalSolution[0][i] = Integer.MAX_VALUE;                //money 找零金额; coinKind 硬币种类,用来表示第几种硬币        for(int money = 1; money <=chargeMoney; money++){            for(int coinKind = 1; coinKind <= coinsKinds; coinKind++){                            //找零金额小于当前硬币面值            if(money < coinsValues[coinKind-1]){            chargeOptimalSolution[coinKind][money] = chargeOptimalSolution[coinKind - 1][money];                    continue;                }                                //不使用第 i(coinkind) 种硬币找零时需要的最小硬币数-- 递推 --            //使用第 i(coinkind) 种硬币时所需的最小硬币数-- 递推 --            int numberByCoinKind = chargeOptimalSolution[coinKind - 1][money];            int numberNotByCoinKind = chargeOptimalSolution[coinKind][money - coinsValues[coinKind-1]] + 1;                                //逻辑判断硬币数目选其中较小的            chargeOptimalSolution[coinKind][money] =             numberByCoinKind < numberNotByCoinKind ? numberByCoinKind : numberNotByCoinKind;            }        }                return chargeOptimalSolution[coinsKinds][chargeMoney];    }        public static void main(String[] args) {        //初始化硬币种类数组    int[] coinsValues = {1,2,5,10,20,50,100};        Arrays.sort(coinsValues);        //初始化找零金额为625        int chargeMoney = 625;        int minCoinsNumber = charge(coinsValues, chargeMoney);        System.out.println("给定找零金额" + chargeMoney        + ",收银员最少的找零硬币数为" + minCoinsNumber);    }}

    2.4 运行结果



原创粉丝点击