因此;我是一个尝试通过SICP(it's free!)工作的业余爱好者,第一章中有一个示例程序,旨在计算用美国硬币进行更改的可能方法; (变更者100)=>它的实现类似于:
(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))
反正;这是一个树递归过程,作者“留下挑战”找到一个迭代过程来解决同样的问题(即固定空间)。我没有幸运得到这个或在沮丧后找到答案。我想知道这对我来说是否是一个大脑放屁,或者是作者是否与我联系。
答案 0 :(得分:12)
通常,消除递归的最简单/最通用的方法是使用辅助堆栈 - 而不是进行递归调用,将其参数压入堆栈,然后迭代。当您需要递归调用的结果以便继续时,再次在一般情况下,这有点复杂,因为您还必须能够推送“继续请求”(将从辅助请求中取消)当结果已知时堆叠);但是,在这种情况下,因为你所有的递归调用结果都是一个求和,所以它足以保持一个累加器,并且每次得到一个数字结果而不需要做更多的调用时,将它添加到累加器。
但是,这本身就是而不是固定空间,因为该堆栈会增长。所以另一个有用的想法是:因为这是一个纯函数(没有副作用),所以当你发现自己计算了某个参数集的函数值时,你可以memoize参数 - 结果对应。这将限制通话次数。导致大致相同计算的另一种概念方法是dynamic programming [[aka DP]],尽管使用DP,您经常自下而上地“准备要记忆的结果”,可以这么说,而不是从递归开始并努力消除它。
例如,在此功能上使用自下而上的DP。你知道你会反复得到“用最小的硬币改变金额X的方法有多少”(当你用原始的amount
中的各种硬币组合将事情简化为X时),所以你开始使用简单迭代计算amount
个值{f(X)= X
/ value
如果X
完全可被最小硬币值value
整除, else 0
;此处value
为1,所以对于所有X> 0,f(X)= X)。现在你继续计算一个新的函数g(X),用两个最小硬币对X进行更改的方法:再次增加X的简单迭代,g(x)= f(X) + g(X - value
)用于第二小硬币的value
(这将是一个简单的迭代,因为当你计算g(X)时,你已经计算并存储了f(X)和对于Y value
),如上所述 - 从现在开始,您将不再需要f(X),因此您可以重用那个空间。总而言之,这需要空间2 * amount
- 而不是“固定空间”,但是,越来越近......
为了实现“固定空间”的最终飞跃,请问自己:你是否需要在每一步(你最后计算的那个和你自己的那个)上保留两个数组的所有值目前正在计算),或者只是某些这些值,通过重新排列你的循环......?
答案 1 :(得分:1)
我想出的解决方案是在'钱包'中记录你正在使用的每种类型的硬币
主循环的工作原理如下; 'denom是当前面额,'改变是钱包中硬币的总价值',给出的是我需要做出的改变量和'清理 - 从钱包中取出比给定面额小的所有硬币
#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的回答后,我提出了钱包的想法,但只是为了让它发挥作用
答案 2 :(得分:1)
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 :(得分:0)
所以,在this thread中,问题的原始提问者通过模块化得出了一个合理的答案。但是,我建议,如果您注意到cc-pennies
完全是多余的(并且扩展名为cc-nothing
),则可以轻松优化其代码。
请注意,编写cc-pennies
的方式的问题在于,因为没有更低的面额,所以通过模仿较高面额程序的结构所做的就是从{{ {1}}到(- amount 1)
,每次从0
程序传递一定金额时,它都会执行此操作。所以,在第一遍中,如果你尝试1美元,你将得到cc-nickels
为100,所以amount
评估为(- amount 1)
,这意味着你将经历99个多余的周期99
和cc-pennies
周期。然后,镍币将以cc-nothing
作为数量传递给您,因此您将获得94个浪费的周期,依此类推。所有这一切都在你将树上升到一角硬币或四分之一或半美元之前。
当你到达95
时,你已经知道你只想把累加器加一,所以我建议改进:
cc-pennies
希望你发现这很有用。
答案 4 :(得分:-2)
您可以在伪多项式时间内使用动态编程迭代求解。