透析递归应用-换零钱

题目源于《SICP》,这里做一下调整,如下:

给了面值为50元、20元、10元、5元、1元的五种零钱若干,思考把面值100元人民币换成零钱一共有多少种方式?

SICP给出的递归算法思想如下:

将总数为a的现金换成n种不同面值的不同方式的数目等于:

  • 将现金a换成除了第一种面值之外的所有其他面值的不同方式数目,加上
  • 将现金a-d换成所有种类的面值的不同方式数目,其中d是第一种面值的钱币

下面有解释到,递归的思想是要将问题归约到对更少现金数或更多种类面值钱币的同一个问题。有如下的约定:

  • 如果a==0,应该算作是有1种换零钱的方式
  • 如果a<0,应该算作是有0中换零钱的方式
  • 如果n=0,应该算作是有0种换零钱的方式

大家先不要纠结于为何要有这种约定,只需要记住这个约定就好了,先看看Lisp代码的实现:

(define (count-change amount)
  (cc amount 5)
)

(define (cc amount kinds-of-coins)
    (cond ((= amount 0) 1)
              ((or (< amount 0)  (= kinds-of-coins 0)) 0)
              (else ( + (cc amount (- kinds-of-coins 1))
                        (cc (- amount (first-denomination kinds-of-coins) kinds-of-coins))
                       )
    )
)

(define (first-denomination kinds-of-coins)
    (cond ((= kinds-of-coins 1) 1)
              ((= kinds-of-coins 2) 5)
              ((= kinds-of-coins 3) 10)
              ((= kinds-of-coins 4) 20)
              ((= kinds-of-coins 5) 50)
    )
)

如果对Lisp有点儿晕,可以看看等价的Java实现:

    //换零钱
    public static int countChange(int mount){
        return cc(mount,5);
    }

    /**
     * @param mount 整钱数量
     * @param coinKinds 零钱类型数量
     */
    private static int cc(int mount, int coinKinds) {
        if(mount == 0 ) return 1;
        if(mount<=0 || coinKinds == 0) return 0;

        return cc(mount,coinKinds - 1) + cc(mount - denomination(coinKinds),coinKinds);
    }
    private static int denomination(int coinKind){
        switch(coinKind){
        case 1:return 1;
        case 2:return 5;
        case 3: return 10;
        case 4: return 20;
        default: return 50;
        }
    }

SICP大赞递归是如何的强大,能将问题简化,初看上面的递归觉得确实如此,但要真正彻底理解上面的代码好像还没那么容易,更别说要自己空手写出上面的代码。

我在看到代码之后,就是不明白为什么会出现下面的代码:

   if(mount == 0 ) return 1;
   if(mount<=0 || coinKinds == 0) return 0;

因为程序是递归的,程序其他地方没出现过return 1,所以可以大概的知道,方法最终得到的换零钱方式数目肯定是这些个1相加得到。

那为什么是mount等于0的时候返回1呢? 需要找个例子,来真正看看程序递归树才知道其中的原因。

为了把问题简化,假设我手头有一张100元的,另外只有两种零钱,分别是50的和20的。这样一来结果好像很明显了,因为换零钱的方式就两种:两个50的或者5个20的。

其实可以更简化,比如就只有一种50的零钱,但那样展示的递归树对帮助我们理解程序不是很明显。

看看下面的递归树:

树节点中左边数字表示amount,右边表示零钱种类。

每一个完整的右斜线代表了全部换成某种面值的尝试;

这些右斜线的左分支代表了换了N个某种面值之后再尝试换其他面值的尝试;

看明白了这个递归树之后,就知道了下面判断条件的意义了:

   if(mount == 0 ) return 1;//整数面值的钱刚好被换完了
   if(mount<=0 || coinKinds == 0) return 0; //该种尝试失败了(零钱加起来比整钱多了),没有可换的零钱种类了

似乎可以把这棵树称为测试树,每个叶子节点代表了测试结果,归结起来就知道成功了多少次。神奇的是递归巧妙地完成了遍历并进行测试。

知道了这种递归其实是在做遍历测试,那我们可以用一种简单而粗暴的测试:

    private static int countChange2(int mount){
        int count = 0;
        int d1 = denomination(1);
        int d2 = denomination(2);
        int d3 = denomination(3);
        int d4 = denomination(4);
        int d5 = denomination(5);
        for(int i=0;i*d1<=mount;i++){
            for(int j=0;j*d2<=mount;j++){
                for(int k=0;k*d3<=mount;k++){
                    for(int l=0;l*d4<=mount;l++){
                        for(int m=0;m*d5<=mount;m++){
                            int test = i * d1
                                     + j * d2
                                     + k * d3
                                     + l * d4
                                     + m * d5;
                            if(test==mount){
                                count++;
                            }
                        }
                    }
                }
            }
        }
        return count;
    }

如果要画出上述算法的运行轨迹,恐怕跟递归树是一样的。并且性能上跟上述递归代码也是一样的。

思考另外一个问题,如果要打印出所有换零钱的方式呢?(而不是方式的总数)

对于上述for循环的遍历,很容易就能得到:

                            if(test==mount){
                                String str = format(d1,i);
                                str += format(d2,j);
                                str += format(d3,k);
                                str += format(d4,l);
                                str += format(d5,m);
                                str = str.substring(0,str.length() - 1);
                                System.out.println(str);
                                count++;
                            }
format方法如下:
private static String format(int d,int count){
        if(count==0){
            return "";
        }
        return " ("+d + "x" + count + ") +";
    }

计算countChange2(10)得到如下结果:

 (10x1)
 (5x2)
 (1x5) + (5x1)
 (1x10) 

而使用递归调用的程序要得到这个结果就稍微麻烦点儿了,因为每次测试成功的时候,“手头”并没有想for循环这样方便的数据。这些数据分布在了递归调用链上。要想拿到这些数据,需要新增一个参数,将调用过程“记录”在这个参数中。

    /**
     * @param mount 整钱数量
     * @param coinKinds 零钱类型数量
     */
    private static int cc(int mount, int coinKinds,String str) {
        if(mount == 0 ) {
            format2(str);
            return 1;
        }
        if(mount<=0 || coinKinds == 0) return 0;

        return cc(mount,coinKinds - 1,str) + cc(mount - denomination(coinKinds),coinKinds,str += "," + coinKinds);
    }
    private static void format2(String str) {
        String[] ds = str.split(",");
        int[] dCount = new int[6];
        for(String dStr :ds){
            if(dStr==null || dStr.equals("")) continue;
            dCount[Integer.parseInt(dStr)]++;
        }
        String res = "";
        for(int i = 1;i<dCount.length;i++){
            if(dCount[i]==0) continue;
            res += " (" + denomination(i) +"x"+dCount[i]  + ") +" ;
        }
        if(res.length()>0) res = res.substring(0,res.length() - 1);
        System.out.println(res);
    }

用一个字符串来记录经过的节点,仔细观察:

cc(mount,coinKinds - 1,str) + cc(mount - denomination(coinKinds),coinKinds,str += "," + coinKinds)

发现为什么左树上面的str没有进行"记录”?原因是,仔细看看递归树就会发现,仅当树往右边走一步的时候才是真正地开启了一次测试之旅。往左的分支表示减少一种面值的钱币,并没开始进行这种测试。

(完)

原创作品,转载时请标注出处地址:http://www.cnblogs.com/huqiaoblog/p/7606664.html

时间: 2024-11-13 06:38:47

透析递归应用-换零钱的相关文章

递归解决换零钱问题--回顾总结之递归的表达能力

前面为了保持叙述的流畅,没有做太多的引申,把总结推迟到了后面. 补上一些总结,以防止出现"下面呢?下面没有了"的尴尬. 方向性问题 虽然题目在一开始就暗示了这一点,但首先,我们还是要问,它能用递归解决吗? 有点怀疑精神是好的,既要低头走路,更要抬头看路,以防止发生方向性错误,导致缘木求鱼的后果. 说这个问题能用递归解决,这种信心或者判断的依据来自于哪呢? 有人可能知道了,换零钱这个问题在<计算机程序的构造和解释>(SICP:Structure and Interpretat

递归练习之换零钱方式统计(c/c++)

/********************************************************************************* Copyright (C), 1988-1999, drvivermonkey. Co., Ltd. File name: Author: Driver Monkey Version: Mail:[email protected] qq:196568501 Date: 2014.04.02 Description: 递归练习之换零钱

SDUT3145:Integer division 1(换零钱背包)

题目:传送门 题目描述 整数划分是一个非常经典的数学问题. 所谓整数划分,是指把一个正整数n写成为n=m1+m2+...+mi的形式,其中mi为正整数,并且1<=mi<=n,此时,{m1, m2, ..., mi}为n的一个划分.如果{m1, m2, ..., mi}中的最大值不超过m,即max{m1, m2, ..., mi}<=m,那么我们称之为整数n的一个m划分. 现在给出你正整数n和m,请你输出n的m划分的数量. 例如,当n=4时,有5个划分,即{4}, {3,1}, {2,2}

关于SICP 1.2.2节中的换零钱方式的统计研究及其迭代实现。

关于SICP 1.2.2节中的换零钱方式的统计研究及其迭代实现 最近开始看sicp(计算机程序的构造和解释)一书,此书竟然是mit的计算机入门教材,不得不令人感叹天朝大学教育与真正一流大学的差距之大..我们在学习c语言的时候,人家已经开始学习剥离具体语言之外的编程思想了..扯远了,说回正题 sicp在1.2.2节中提到了一个有意思的换零钱实例: 将1美元(100美分)换成半美元,1/4美元,10美分,5美分,1美分的零钱,一共有多少种换法? 初看感觉有点无从下手,脑子里想的是各种排列组合,一片混

动态规划题目(一)——换零钱

动态规划题目(一)--换零钱 1. 题目描述 想兑换100元钱,有1,2,5,10四种钱,问总共有多少兑换方法. 下面提供两种实现方式,其中代码注释的很清楚. 关于动态规划的基本原理,参考: http://www.cnblogs.com/sdjl/articles/1274312.html 2. 递归解法 //动态规划 #include<iostream> using namespace std; const int N = 100; int dimes[] = {1, 2, 5, 10};

从经典架构项目中透析微服务架构的核心概念和充血模型

微服务架构和SOA区别 微服务现在辣么火,业界流行的对比的却都是所谓的Monolithic单体应用,而大量的系统在十几年前都是已经是分布式系统了,那么微服务作为新的理念和原来的分布式系统,或者说SOA(面向服务架构)是什么区别呢? 我们先看相同点: 需要Registry,实现动态的服务注册发现机制:需要考虑分布式下面的事务一致性,CAP原则下,两段式提交不能保证性能,事务补偿机制需要考虑:同步调用还是异步消息传递,如何保证消息可靠性?SOA由ESB来集成所有的消息:都需要统一的Gateway来汇

贪心算法换零钱(java)

贪心算法思想 贪心算法总是做出在当前看来做好的选择.也就是说贪心算法并不从整体最后考虑,他做出的选择只是局部最优选择.他所做出的仅是在某种意义上的局部最优解.贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解. 1.算法思路 贪心算法是一种不追求最优解,只希望得到较为满意解的方法.贪心算法一般可以快速得到满意的解,因为它省去了为找最优姐要穷尽所有肯呢个而必须耗费大量时间.贪婪(心)算法是一种改进了的分级处理方法.其核心是根据题意选取一种

小P的故事——神奇的换零钱&amp;&amp;人活着系列之平方数

http://acm.sdut.edu.cn/sdutoj/showproblem.php?pid=2777&cid=1219 这题不会,看了别人的代码 #include <iostream> #include <string.h> #include <stdlib.h> #include <stdio.h> using namespace std; int dp[32770]; int main() { int n,i,j; int w[4]= {

透析Java本质-谁创建了对象,this是什么

Android系统手机屏幕的左上角为坐标系,同时y轴方向与笛卡尔坐标系的y轴方向想反.通过提供的api如getLeft , getTop, getBottom, getRight可以获得控件在parent中的相对位置.同时,也可以获得控件在屏幕中的绝对位置,详细用法可参考android应用程序中获取view的位置 当我们编写一些自定义的滑动控件时,会用到一些api如scrollTo(),scrollBy(),getScrollX(), getScrollY().由于常常会对函数getScroll