SICP做出改变

Posted

技术标签:

【中文标题】SICP做出改变【英文标题】:SICP making change 【发布时间】:2010-12-01 21:13:40 【问题描述】:

所以;我是一个正在尝试通过 SICP (it's free!) 工作的业余爱好者,第一章中有一个示例程序,旨在计算用美国硬币找零的可能方法; (change-maker 100) => 292. 它的实现类似于:

(define (change-maker amount)
  (define (coin-value n)
    (cond ((= n 1) 1)
          ((= n 2) 5)
          ((= n 3) 10)
          ((= n 4) 25)
          ((= n 5) 50)))

  (define (iter amount coin-type)
    (cond ((= amount 0) 1)
          ((or (= coin-type 0) (< amount 0)) 0)
          (else (+ (iter amount
                         (- coin-type 1))
                   (iter (- amount (coin-value coin-type))
                         coin-type)))))

  (iter amount 5))

无论如何;这是一个树递归过程,作者“作为一个挑战离开”找到一个迭代过程来解决相同的问题(即固定空间)。在感到沮丧之后,我没有运气弄清楚这一点或找到答案。不知道是不是我脑洞大开,还是作者在逗我呢。

【问题讨论】:

c2.com/cgi/wiki?SicpIterationExercise 详细讨论了这个问题,并在最后有一个或多或少完整的解决方案。 【参考方案1】:

一般来说,消除递归的最简单/最通用的方法是使用辅助堆栈——而不是进行递归调用,而是将它们的参数推入堆栈并进行迭代。当您需要递归调用的结果才能继续时,同样在一般情况下,这有点复杂,因为您还必须能够推送“继续请求”(这将脱离辅助结果已知时堆栈);但是,在这种情况下,由于您对所有递归调用结果所做的只是求和,因此保留一个累加器就足够了,并且每次得到一个数字结果而不是需要进行更多调用时,将其添加到累加器。

但是,这本身不是固定空间,因为堆栈会增长。所以另一个有用的想法是:因为这是一个纯函数(没有副作用),任何时候你发现自己已经计算了某个参数集的函数值,你可以memoize参数结果对应。这将限制调用次数。导致相同计算的另一种概念方法是dynamic programming [[aka DP]],尽管使用 DP,您通常可以自下而上地“准备要记忆的结果”,而不是从递归开始工作消除它。

以这个函数的自底向上 DP 为例。你知道你会反复得到“用最小的硬币有多少种方法可以改变金额 X”(当你用原始 amount 的各种硬币组合将事情缩减到 X 时),所以你开始计算那些amount 具有简单迭代的值 (f(X) = X/value 如果 X 可以被最小硬币值 value 整除,否则 0;这里,value 是 1 ,所以对于所有 X>0,f(X)=X)。现在您继续计算一个新函数 g(X),用 两个 最小硬币对 X 进行更改的方法:再次简单迭代增加 X,其中 g(x) = f(X) + g(X - value) 代表第二小的硬币的value(这将是一个简单的迭代,因为当你计算 g(X) 时,你已经计算并存储了 f(X) and 对于 Y 三个 最小硬币为 X 找零的方法 -- h(X) = g(X) + g(X-value) 同上 --从现在开始,您将不再需要 f(X),因此您可以重用 那个 空间。总而言之,这需要空间2 * amount——还不是“固定空间”,但是,越来越近了......

要实现“固定空间”的最后飞跃,请问自己:您是否需要在每一步都保留两个数组的 all 值(您最后计算的一个和您正在计算的一个当前正在计算),或者,只有 一些 这些值,通过重新安排你的循环一点点......?

【讨论】:

我对您在这里提到的迭代方法几乎没有疑问。如果你澄清他们会很高兴: 1. 你说 f(X) = X/value,如果 X 可以被最小的硬币值整除,否则为 0。不应该是 f(X) = 1,如果 X完全可以被值整除,否则为 0? 2.如果我对上述1的理解是正确的,我们如何修改这种方法以找到原始金额所需的“最小硬币数量”? 1.我认为如果 X 完全可以被值整除,f(X) 应该是 1。 2.我认为f(0), g(0), h(0)也应该是1,因为g(5) = f(5) + g(0), g(5)应该是2(2种方式用 1 和 5 美分换 5 美分)。 3. 我们知道 g(5) = 2,我们可以知道 g(10) = f(10) + g(5) = 3,所以 g(100) = 21. 4.h(10) = g( 10) + h(0) = 4, h(20) = g(20) + h(10),这样我们就可以用循环来计算h(100),如i(100),取值是 25,然后是 j(100),其值为 50,这将是数字 292 除了上面的cmets之外,我想指出方程“h(X) = g(X) + g(X-value)”应该是“h(X) = g(X) + h(X-value)",据我所知。【参考方案2】:

Here 是我的函数版本,使用动态编程。一个大小为 n+1 的向量被初始化为 0,除了第 0 项最初为 1。然后对于每个可能的硬币(外部 do 循环),每个向量元素(内部 do 循环)从第 k 个开始,其中 k 是硬币的价值,按当前索引处的值减去 k 递增。

(define (counts xs n)
  (let ((cs (make-vector (+ n 1) 0)))
    (vector-set! cs 0 1)
    (do ((xs xs (cdr xs)))
        ((null? xs) (vector-ref cs n))
      (do ((x (car xs) (+ x 1))) ((< n x))
        (vector-set! cs x (+ (vector-ref cs x)
          (vector-ref cs (- x (car xs)))))))))

> (counts '(1 5 10 25 50) 100)
292

你可以在http://ideone.com/EiOVY运行这个程序。

【讨论】:

【参考方案3】:

因此,在this thread 中,问题的原始提问者通过模块化提出了合理的答案。但是,我建议,如果您注意到 cc-pennies 完全是多余的(并且通过扩展,cc-nothing 也是如此),他的代码可以轻松优化

看,cc-pennies 的编写方式的问题在于,因为没有更低的面额可走,它所要做的就是模仿更高面额程序的结构,从(- amount 1) 向下迭代到0 ,并且每次您从cc-nickels 过程中传递一个金额时,它都会执行此操作。因此,在第一次通过时,如果您尝试 1 美元,您将得到 100 的 amount,因此 (- amount 1) 的计算结果为 99,这意味着您将经历 cc-pennies 和 @987654332 的 99 个多余循环@ 循环。然后,镍币将作为金额传递给您95,因此您会多浪费 94 个周期,依此类推。而这一切都在你向上移动到一角钱、四分之一美元或半美元之前。

当您到达cc-pennies 时,您已经知道您只想将累加器加一,所以我建议您进行以下改进:

(define (count-change-iter amount)
    (cc-fifties amount 0))

(define (cc-fifties amount acc)
    (cond ((= amount 0) (+ 1 acc))
        ((< amount 0) acc)
        (else (cc-fifties (- amount 50)
                (cc-quarters amount acc)))))

(define (cc-quarters amount acc)
    (cond ((= amount 0) (+ 1 acc))
        ((< amount 0) acc)
        (else (cc-quarters (- amount 25)
                (cc-dimes amount acc)))))

(define (cc-dimes amount acc)
    (cond ((= amount 0) (+ 1 acc))
        ((< amount 0) acc)
        (else (cc-dimes (- amount 10)
                (cc-nickels amount acc)))))

(define (cc-nickels amount acc)
    (cond ((= amount 0) (+ 1 acc))
        ((< amount 0) acc)
        (else (cc-nickels (- amount 5)
                (cc-pennies amount acc)))))

(define (cc-pennies amount acc)
    (+ acc 1))

希望你觉得这很有用。

【讨论】:

【参考方案4】:

我想出的解决方案是记录你在“钱包”中使用的每种硬币的数量

主循环是这样工作的; 'denom 是当前面额,'changed 是钱包中硬币的总价值,'given 是我需要做的零钱,'clear-up-to 将所有小于给定面额的硬币从钱包中取出.

#lang scheme
(define (sub changed denom)
  (cond
   ((> denom largest-denom)
    combinations)

   ((>= changed given)
    (inc-combinations-if (= changed given))

    (clear-up-to denom)
    (jump-duplicates changed denom)) ;checks that clear-up-to had any effect.

   (else
    (add-to-purse denom)
    (sub
     (purse-value)
     0
     ))))

(define (jump-duplicates changed denom)
  (define (iter peek denom)
    (cond
     ((> (+ denom 1) largest-denom)
      combinations)

     ((= peek changed)
      (begin
        (clear-up-to (+ denom 1))
        (iter (purse-value) (+ denom 1))))

      (else
       (sub peek (+ denom 1)))))
  (iter (purse-value) denom))

在阅读了 Alex Martelli 的回答后,我想出了钱包的想法,但只是想办法让它发挥作用

【讨论】:

【参考方案5】:

您可以在伪多项式时间内使用动态规划迭代求解。

【讨论】:

以上是关于SICP做出改变的主要内容,如果未能解决你的问题,请参考以下文章

当状态改变时,对文本字段做出反应 onChange 更新整个组件

html 能否用js对table样式做出改变

3.5星|《行为设计学:零成本改变》:明确的可操作的短期的可以引起情感共鸣的目标,更有助于个人或组织做出改变

如何在不改变状态的情况下对 redux 操作做出反应

当 useState 改变 react-complex-tree 不重新渲染时做出反应

wpf 改变属性后怎么自动做出响应?