SICP例子:计数变化,无法理解

时间:2015-01-06 16:41:50

标签: algorithm recursion scheme sicp coin-change

我是麻省理工学院开放式课程的SICP课程的初学者,使用视频讲座和在线提供的书籍。昨天我遇到了一个例子,询问我们是否可以写一个程序来计算改变任何给定金额的方法的数量。

这个问题有一个简单的解决方案作为递归过程:

(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) 25)
        ((= kinds-of-coins 5) 50)))

如果你想查看更多内容,我是从here获取的。

他们正在使用K种硬币计算改变数量(A)的数量(N):

  1. 在没有第一类硬币的情况下改变A的方式(X)的数量。

  2. 改变(A - D)的方式(Y)的数量,其中D是fisrt硬币的面额,使用所有K种类型的硬币。

  3. 问题是,我只是不明白这一点。接着,他们试图解释说:

    &#34;要知道为什么这是真的,请注意改变的方法可以分为两组:不使用任何第一种硬币的那些,以及那些使用任何硬币的方法。因此,在不使用任何第一种硬币的情况下,改变金额的方式的数量等于改变金额的方式的数量,加上假设我们使用的方式进行更改的方式的数量。第一种硬币。 (最后一句与添加N = X + Y相同吗?)但是后一个数等于使用第一个硬币后剩余金额的变化方式的数量类。 (使用这枚硬币之后,它们指的是使用或不使用第一种硬币进行更改的方式?)&#34;

    我理解他们是如何实现递归算法的,但是我无法看到它们是如何实现的。 英语不是我的母语,所以我可能会遗漏一些东西。如果您能解释我,使用其他术语,解决方案背后的逻辑我会非常感激。感谢。

3 个答案:

答案 0 :(得分:13)

&#34;数量(N)的方式......使用N种&#34;这两个N显然不一样。所以,让我们说K种硬币。

我们有很多硬币,但每枚硬币分别为1,5,10,25或50美分,总共5种硬币。我们需要买一美元,100美分。假设每种硬币都能无限量供应。我们有多少种方法可以达到100的总和?

我们要么使用50美分的硬币(一个或多个),要么不要。如果没有,我们仍然需要只用4种硬币到达100。但是如果我们这样做,那么在使用一个50美分硬币之后,总金额是100 - 50 = 50美分,我们仍然可以使用所有5种硬币来达到新的,更小的总和:

ways{ 100, 5 } = ways{ 100, 5 - 1 }      ;   never use any 50-cent coins
                 +                       ; OR
                 ways{ 100 - 50,  5 }    ;   may use 50-cent coins, so use one

或者一般来说,

ways( sum, k ) = ways( sum, k - 1 )
                 +
                 ways( sum - first_denomination(k),  k )

这就是它的全部内容。看到?泛化自然来自抽象(用符号替换具体值并在函数定义中使它们成为参数)。


然后我们需要关心基本情况。如果sum = 0,则结果为1:有一种方法可以达到总和0(并且它是:不接受硬币)。

如果k = 0,则表示我们不允许使用任何种硬币;换句话说,我们无法达到总和,任何总和,而不使用至少一些硬币(除非总和为0,但我们&# 39;已经处理过上述情况)。所以结果必须是0。

当然,sum < 0相同。不可能,即0种方式来总结它,使用任何正面额的硬币。


(结构)递归指向小 - 而不是试图一次性描绘整个操作序列,而是静止不动并试图理解你的当前< / em>情况。它是解决问题的心理工具,它是以最简单最自然的方式解决问题,尽可能小的步骤。

自己致电(a copy of)是一种技术性问题。主要的是信念的飞跃,你是允许给自己打电话:假设你已经写下了你的定义,只需使用它就是合适的。那是how it gets written down。你只需要描述你拥有的东西,它是如何由它的部分构成的(其中一些类似完整的东西),以及如何将这些部分的结果组合起来以获得完整的解决方案因为你有。


编辑(来自评论):递归地解决问题的关键是要认识到它可以被分解为一个较小的子问题的集合,每个子问题相同的一般我们正在寻求的解决程序适用,然后以一些简单的方式从那些子问题中找到整体解决方案。解决方案(通过与我们已经可用的相同的一般程序找到)。由此产生的每个子问题都是“更小”的问题。保证最终达到基本情况。

换句话说,尝试在问题中找到结构,使其具有与整体相似的子结构(如分形;例如列表的后缀也是列表;等等);然后,递归是:假设我们已经有了解决方案;将问题实例分开(根据我们构建问题的方式);通过解决方案转换&#34;较小的&#34; 子结构;将它们以一些简单的方式组合在一起(根据我们构建问题的方式)。诀窍是识别问题中的现有,固有结构,以便解决方案自然而然。

或者,在 Prolog

recursion( In, Out) :- 
  is_base_case( In), 
  base_relation( In, Out).

recursion( In, Out) :- 
  is_not_base_case( In),
  constituents( In, SelfSimilarParts, LeftOvers),        % forth
  maplist( recursion, SelfSimilarParts, InterimResults),
  constituents( Out, InterimResult, LeftOvers).          % and back

答案 1 :(得分:5)

如果我们对递归过于刻苦,我们就已经失败了。就个人而言,我在思考递归时使用了两个隐喻。一个来自小书“小阴谋家”:The Seventh Commandment - Recur on the subparts that are of the same nature。另一个是用于设计算法的分治 - 组合范式。从本质上讲,它们在如何递归思考方面是一样的。

  1. 分为相同性质的子部分
  2. 问题有两个变量:货币的数量(N)和硬币的种类(K),因此任何分工都需要满足以下要求:1. reducing all variables: both N and K, 2. the subparts are the same nature so each subpart can be solved by the recursion process itself or be can solved directly. 3. all subparts together == the original one part, no more and no less.

    解决方案中的划分将原始问题分为两个子部分:第一个子部分是使用第一个硬币的所有组合(我们可以重申所有组合使用相同含义的第一个硬币的至少一个硬币)。剩下的部分是所有组合都不使用第一枚硬币。 N在第一子部分减少,K在第二部分减少。两者都是相同的性质,可以递归地解决,它们是最初的问题。

    1. 在这一步中,我考虑了基本情况。当问题减少到可以直接给出答案的最小值时,所有基本情况是什么?在这个解决方案中,有三种基本情况。第一个是N减少到0.第二个是N减少到负数。第三是硬币减少到0但N仍然是正值。

      1. 结合
      2. 解决子部件时如何组合结果。在这个解决方案中,它们只是+。

        更重要的是,如果我们在列表中递归,则除法通常是列表的车和列表的cdr。如果自己不是列表,通常可以直接解决列表中的汽车。 cdr部分应该递归解决。如果符合,基本案例就是列表的结尾。

        B.T.W,我强烈建议the little schemer学习递归。就我而言,就这一点而言,它比其他任何人都要好得多。

答案 2 :(得分:4)

上面Will Ness回答的第一个代码框给了我足够的洞察力来理解算法。一旦我理解了它,我就意识到我可能已经很快就能通过实际看到算法的步骤来实现这一目标。

下面是算法如何针对简单情况进行的图表。金额是6便士,我们有两种硬币:五便士(指数2)和便士(指数1)。

请注意,叶节点都计算为0或1.当我们查看过程中的条件时(这些值中的一个被返回,或者函数再次调用自身),这是显而易见的。)只有两个叶节点计算到1,所以有两种方法可以从这两种硬币制作6便士,即6便士,或一便士和五便士。

我现在理解算法,但我仍然没有看到我如何根据最初的问题计算出算法。也许,当我阅读更多关于SICP的书时,这种解决方案对我来说似乎更为明显。

                             (cc 6 2)
                                |
                 -----------------------------------
                 |                                 |
              (cc 6 1)                          (cc 1 2)
                 |                                 |
   ------------------                         --------------
   |                |                         |            |
(cc 6 0)=0       (cc 5 1)                  (cc 1 1)     (cc -4 2)=0
                    |                         |
             -------------             -------------
             |           |             |           |
          (cc 5 0)=0  (cc 4 1)      (cc 1 0)=0  (cc 0 1)=1
                         |
               --------------
               |            |
            (cc 4 0)=0   (cc 3 1)
                            |
                     --------------
                     |            |
                  (cc 3 0)=0   (cc 2 1)
                                  |
                           --------------
                           |            |
                        (cc 2 0)=0   (cc 1 1)
                                        |
                                 --------------
                                 |            |
                              (cc 1 0)=0   (cc 0 1)=1