【问题标题】:SICP example: Counting change, cannot understandSICP例子:数变化,看不懂
【发布时间】:2015-03-04 09:14:43
【问题描述】:

我是一名初学者,正在学习麻省理工学院 OpenCourseWare 上的 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 是第一枚硬币的面额,使用所有 K 种硬币。

问题是,我就是不明白。接下来,他们试图解释说:

“要了解为什么会这样,请观察做出改变的方式可以分为两组:不使用任何第一种硬币的方式和使用任何一种硬币的方式。因此,方式的总数找零等于不使用任何第一种硬币的情况下找零的方法的数量,加上假设我们使用第一种硬币的找零的方法的数量。(最后一句是否与加法N = X + Y 相同?) 但是后一个数字等于使用第一种硬币后剩余数量的找零方式的数量。 (使用此币后,指的是有无第一种硬币的找零方式?) "

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

【问题讨论】:

  • 以粗体表示您的第一个问题:是的。到第 2 次:用它(回复:“使用所有 K 种硬币”)。什么还不清楚?
  • @WillNess 我现在理解得更清楚了,谢谢,现在,我无法完全理解书中给我的问题中的 Y 值提供逻辑的部分:“因此,总数找零的方法数等于不使用任何第一种硬币的情况下找零的方法的数量,加上假设我们使用第一种硬币的找零的方法的数量。 但后一个数字等于使用第一种硬币后剩余金额的找零方式的数量这是为什么?我看不到它。
  • @nanameable, do use the first kind of coin 可以重述 do use at least a coin of the first kind in the same meaning.求解为(cc(-金额(第一面额种类硬币))种类硬币)))))
  • (只是为了在这里清楚地说明一半:我已经编辑了my answer,以解决上面针对我的评论)

标签: algorithm recursion scheme sicp coin-change


【解决方案1】:

“方式数(N)......使用N种”这两个Ns显然不一样。所以让我们说K种硬币。

我们有很多硬币,但每个硬币是 1、5、10、25 或 50 美分,共有 5 种硬币。我们需要花一美元买东西,100 美分。假设每种硬币无限供应。我们有多少种方法可以达到 100 的总和?

我们要么使用一些 50 美分的硬币(一个或多个),要么不使用。如果没有,我们仍然只能用 4 种硬币达到 100。但是如果我们这样做了,那么在使用one 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,这意味着我们不允许使用任何种硬币;换句话说,如果不使用至少 一些 个硬币,我们就无法得出一个总和,any sum(除非总和为 0,但我们已经处理过上例)。所以结果一定是0。

当然,sum &lt; 0 也一样。不可能,即 0 种方式来总结它,使用任何正面额的硬币。


如果你愿意,另一种方式是从时间箭头的另一端。

想象一下,有人已经为您完成了所有这些工作,并将所有这些成堆的账单放在您面前,每一堆加起来就是目标金额。不失一般性,让每一堆都被排序,以便更大的钞票在上面。

将所有纸币分成两组:一组纸币的顶部面额最大,另一组没有纸币。如果总堆数是ways( denomsList, targetSum),那么显然第二组的堆数是ways( rest(denomsList), targetSum)

然后,我们可以从第一组中的每一堆中取出最上面的钞票,而其中的堆数显然不会因此而改变。删除每一堆中最上面的钞票后,我们看到它们的总和为targetSum - first(denomsList),因此它们的总数为ways( denomsList, targetSum - first(denomsList))


(结构性)递归的要点是从小处思考——不是试图立即描绘整个操作序列,而是静止不动并试图理解你的当前 情况。它是解决问题的一种心理工具,它是关于以最简单最自然的方式解决问题,使步骤尽可能小

给自己打电话 (a copy of) 是一种技术问题。最重要的是信仰的飞跃,你被允许称自己为:假设你已经写下你的定义,使用它是合适的。那是how it gets written down。您只需描述您拥有的东西,它是如何由 较小 部分组成的(其中一些 类似于 与完整的东西),以及 结果 如何那些部分可以与其余部分组合回来以获得完整的解决方案。


edit(来自 cmets):递归解决问题的关键是认识到可以将其分解为较小的子问题的集合,每个子问题具有相同的一般性我们正在寻求的求解过程适用,然后以某种简单的方式从那些子问题的解决方案中找到总解决方案(通过相同的一般程序找到,就好像它是我们已经可以使用了)。这样创建的每个子问题都“更小”,保证最终会达到基本情况。

换句话说,尝试找到问题中的结构,使其具有与整体相似的子结构(如分形;或者例如,列表的后缀也是列表;等等);那么,递归是:假设我们已经有了解决方案; 采取问题实例分开(根据我们构建问题的方式); 转换解决方案的“较小的”子结构;然后以某种简单的方式组合全部返回(根据我们构建问题的方式)。诀窍是识别问题中现有的固有结构,以便解决方案自然而然。

或者,在Prolog(所有编程语言中:)):

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

recursion(       In,  Out) :- 
  not_base_case( In),
  constituents(  In,   SelfSimilarParts,       LeftOvers),  % (* forth >>>    *)
  maplist( recursion,  SelfSimilarParts,
                             InterimResults),
  constituents(       Out,   InterimResults,   LeftOvers).  % (* and back <<< *)

也就是说,在伪代码中,

(In <--> Out) are related by recursion when
   either
      In is indivisible, and Out its counterpart
   or
      In  =  Sub_1 <+> Sub_2 <+> ... <+> Sub_N  <++>  Shell
             ------  r e c u r s i o n  ------
      Out =  Res_1 {+} Res_2 {+} ... {+} Res_N  {++}  Shell
        where
        (Sub_i <--> Res_i) ,  for each  i = 1, ..., N

InOut 的组合运算 + 可能不同,因为它们可以是不同类型的值。

【讨论】:

  • “我们要么使用一些 50 美分的硬币(一个或多个),要么不使用。” - 我有点困惑,在“或不”部分。为什么我们会假设我们可能不想使用第一种?为什么我们要避免第一种计算方式?
  • @morbidCode 这是一种可能性。我们想要达到 100。我们可以尝试使用 50 美分硬币,或不使用。两者都可以是一个有效的选择,直到我们尝试才知道。因此,我们尝试两种情况 - 一次使用,另一次不使用 - 并通过将两种情况下的成功次数相加来计算总体成功次数。
  • 啊,既然我们是递归的,首先我们尝试用 50 美分硬币来做这件事,我们要么使用 50 美分,要么不使用。然后在这两个过程中,我们将对下一个面额 25 美分硬币做同样的事情。在每一个 25 美分硬币中,我们要么使用那 25 美分硬币,要么不使用。然后在每个过程中,我们对其余的教派做同样的事情。那是对的吗?但看起来我们必须多次计算相同的值?
  • 知道了!这很漂亮。谢谢。
  • 最好的解释之一:问题和递归
【解决方案2】:

如果我们在递归上想得太多,我们就已经失败了。就个人而言,我在思考递归时使用了两个隐喻。一本来自小书《小阴谋家》:The Seventh Commandment - Recur on the subparts that are of the same nature。另一个是用于设计算法的分而治之的范式。本质上,它们在如何递归思考方面是相同的。

  1. 划分为相同性质的子部分

问题有两个变量:货币的数量(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. 结合

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

更重要的是,如果我们对一个列表进行递归,则划分通常是列表的 car 和列表的 cdr。通常列表的汽车如果本身不是列表,可以直接解决。 cdr 部分应该递归解决。如果满足,基本情况就是列表的结尾。

顺便说一句,我强烈推荐the little schemer 来学习递归。据我所知,在这一点上它比其他任何人都好。

【讨论】:

    【解决方案3】:

    上述 Will Ness 回答中的第一个代码框让我有足够的洞察力来理解算法。一旦我理解了它,我意识到通过实际一步一步地查看算法的作用,我可能会很快到达那里。

    下面是一个简单案例的算法如何进行的图表。金额是 6 便士,我们有两种硬币:五便士(索引 2)和便士(索引 1)。

    请注意,叶节点的计算结果都为 0 或 1。当我们查看过程中的条件时,这一点很明显(返回其中一个值,否则函数再次调用自身。)只有两个叶节点计算为1,所以这两种硬币有两种方法可以制作6便士,即6便士,或1便士和5便士。

    我现在理解了算法,但我仍然不明白如何从最初的问题中得出算法。也许,当我阅读更多 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
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-18
    • 2011-11-14
    • 1970-01-01
    • 1970-01-01
    • 2019-12-20
    • 1970-01-01
    相关资源
    最近更新 更多