SICP making change
Asked Answered
A

5

14

So; I'm a hobbyist who's trying to work through SICP (it's free!) and there is an example procedure in the first chapter that is meant to count the possible ways to make change with American coins; (change-maker 100) => 292. It's implemented something like:

(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))

Anyway; this is a tree-recursive procedure, and the authors "leave as a challenge" finding an iterative procedure to solve the same problem (i.e. in fixed space). I have not had luck figuring this out or finding an answer after getting frustrated. I'm wondering if I'm just stuck, or the authors are playing with me.

Associative answered 28/9, 2009 at 1:28 Comment(1)
c2.com/cgi/wiki?SicpIterationExercise discusses this at length and has a more-or-less complete solution at the end.Finery
U
13

The simplest / most general way to eliminate recursion, in general, is to use an auxiliary stack -- instead of making the recursive calls, you push their arguments into the stack, and iterate. When you need the result of the recursive call in order to proceed, again in the general case, that's a tad more complicated because you're also going to have to be able to push a "continuation request" (that will come off the auxiliary stack when the results are known); however, in this case, since all you're doing with all the recursive call results is a summation, it's enough to keep an accumulator and, every time you get a number result instead of a need to do more call, add it to the accumulator.

However, this, per se, is not fixed space, since that stack will grow. So another helpful idea is: since this is a pure function (no side effects), any time you find yourself having computed the function's value for a certain set of arguments, you can memoize the arguments-result correspondence. This will limit the number of calls. Another conceptual approach that leads to much the same computations is dynamic programming [[aka DP]], though with DP you often work bottom-up "preparing results to be memoized", so to speak, rather than starting with a recursion and working to eliminate it.

Take bottom-up DP on this function, for example. You know you'll repeatedly end up with "how many ways to make change for amount X with just the smallest coin" (as you whittle things down to X with various coin combinations from the original amount), so you start computing those amount values with a simple iteration (f(X) = X/value if X is exactly divisible by the smallest-coin value value, else 0; here, value is 1, so f(X)=X for all X>0). Now you continue by computing a new function g(X), ways to make change for X with the two smallest coins: again a simple iteration for increasing X, with g(x) = f(X) + g(X - value) for the value of the second-smallest coin (it will be a simple iteration because by the time you're computing g(X) you've already computed and stored f(X) and all g(Y) for Y < X -- of course, g(X) = 0 for all X <= 0). And again for h(X), ways to make change for X with the three smallest coins -- h(X) = g(X) + g(X-value) as above -- and from now on you won't need f(X) any more, so you can reuse that space. All told, this would need space 2 * amount -- not "fixed space" yet, but, getting closer...

To make the final leap to "fixed space", ask yourself: do you need to keep around all values of two arrays at each step (the one you last computed and the one you're currently computing), or, only some of those values, by rearranging your looping a little bit...?

Upset answered 28/9, 2009 at 2:16 Comment(3)
I have few doubts with the iterative approach you have mentioned here. Would be glad if you clarified them: 1. You said f(X) = X/value, if X is exactly divisible by the smallest-coin value, else 0. Shouldn't it be f(X) = 1, if X is completely divisible by value, else 0? 2. If my understanding about 1 above is correct, How do we modify this approach to find the "minimum number of coins" needed to make change for original amount?Pretentious
1. I think f(X) should be 1 if X is exactly divisible by value. 2. I think f(0), g(0), h(0) should be 1 too, as g(5) = f(5) + g(0), and g(5) should be 2(2 ways to change 5 cents using 1 and 5 cent). 3. As we know g(5) = 2, we can tell g(10) = f(10) + g(5) = 3, so that g(100) = 21. 4.h(10) = g(10) + h(0) = 4, h(20) = g(20) + h(10), in this way, we can use a loop to compute h(100), so as i(100), which value is 25, and then j(100), which value is 50, and that will be the number 292Rebecarebecca
In addition to the comments above, I'd like to point out that the equation "h(X) = g(X) + g(X-value)" should be "h(X) = g(X) + h(X-value)", as far as I can see.Livvi
S
3

Here is my version of the function, using dynamic programming. A vector of size n+1 is initialized to 0, except that the 0'th item is initially 1. Then for each possible coin (the outer do loop), each vector element (the inner do loop) starting from the k'th, where k is the value of the coin, is incremented by the value at the current index minus 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

You can run this program at http://ideone.com/EiOVY.

Sandal answered 13/4, 2012 at 13:7 Comment(0)
H
3

So, in this thread, the original asker of the question comes up with a sound answer via modularization. I would suggest, however, that his code can easily be optimized if you notice that cc-pennies is entirely superfluous (and by extension, so is cc-nothing)

See, the problem with the way cc-pennies is written is that, because there's no lower denomination to go, all it will do by mimicking the structure of the higher denomination procedures is iterate down from (- amount 1) to 0, and it will do this every time you pass it an amount from the cc-nickels procedure. So, on the first pass, if you try 1 dollar, you will get an amount of 100, so (- amount 1) evaluates to 99, which means you'll undergo 99 superfluous cycles of the cc-pennies and cc-nothing cycle. Then, nickels will pass you 95 as amount, so you get 94 more wasted cycles, so on and so forth. And that's all before you even move up the tree to dimes, or quarters, or half-dollars.

By the time you get to cc-pennies, you already know you just want to up the accumulator by one, so I'd suggest this improvement:

(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))

Hope you found this useful.

Hanselka answered 9/5, 2018 at 19:37 Comment(0)
A
1

The solution I came up with is to keep count of each type of coin you're using in a 'purse'

The main loop works like this; 'denom is the current denomination, 'changed is the total value of coins in the purse, 'given is the amount of change I need to make and 'clear-up-to takes all the coins smaller than a given denomination out of the purse.

#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))

After reading Alex Martelli's answer I came up with the purse idea but just got around to making it work

Associative answered 2/5, 2010 at 0:9 Comment(0)
W
-2

You can solve it iteratively with dynamic programming in pseudo-polynomial time.

Wende answered 29/4, 2010 at 21:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.