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