I have discovered what I believe is a nice optimization for the tree-recursive change-counting procedure,
cc, mentioned in a previous post. (In the first edition, it’s Exercise 1.9.)
Unfortunately I’m still struggling with the real problem: how to implement this problem to evolve an iterative process, as requested by the book. My attempts to recast the computation using only variables passed as procedure arguments are failing. These attempts always need more “registers” than I have available, and I start thinking in terms of some kind of “storage” where I can stash the intermediate problems that I can’t work on yet.
Of course, these “registers” are arguments to the function, and this “storage” is also known as the stack. And growing the stack is what we’re trying to avoid.
Scannning ahead in the text, Exercise 1.10 asks the reader to
Draw the tree illustrating the process generated by the (
cc) procedure … making change for 11 cents.
Here’s what appeared in my notebook – Note that the procedure calls surrounded by boxes are the ones that return a value of 1. In this case, the return value of
(cc 11 3) is 4, shown by the 4 boxed procedure calls:
While drawing out this tree, I noticed that once the procedure tries to make change for an
amount using only pennies, it makes
(* 2 amount) unnecessary procedure calls. It’s easy to see that they just bang down the list from
amount to 0 and finally return 1. My idea was to reformulate the procedure so that we perform this check first, like so:
(define (cc-faster amount kinds-of-coins) (cond ((= kinds-of-coins 1) 1) ; Are we making change with pennies? If so, there's only one way to do it! ((= amount 0) 1) ((or (< amount 0) (= kinds-of-coins 0)) 0) (else (+ (cc-faster amount (- kinds-of-coins 1)) (cc-faster (- amount (first-denomination kinds-of-coins)) kinds-of-coins)))))
Calling this procedure results in the following tree of procedure calls:
Note that instead of 55 procedure calls, we now make only 15! And the discrepancy only grows wider as we count larger amounts of change, prompting me to make the following measurements using MIT Scheme – the
with-timing-output macro is described at the bottom:
(with-timing-output (cc 700 5)) ; Run time: 75.81 ; GC time: .34 ; Actual time: 76.641 ;Value: 207530 (with-timing-output (cc-faster 700 5)) ; Run time: .56 ; GC time: .02 ; Actual time: .608 ;Value: 207530
This reduces running time from over a minute to less than a second, which is kind of exciting! It’s still not an iterative process, though.
Just for completeness, here’s how long the same procedure takes using
m-cc, the memoizing version of
cc described previously. Obviously looking stuff up in memory is faster than computing it over and over again:
(with-timing-output (m-cc 700 5)) ; Run time: 0. ; GC time: 0. ; Actual time: 0. ;Value: 207530
And here’s the definition of the
with-timing-output macro. It’s just sugar on top of the
with-timings measurement procedure built into MIT Scheme:
(define-syntax with-timing-output (syntax-rules () ((with-timing-output body) (with-timings (lambda () body) (lambda (run-time gc-time real-time) (display "Run time:\t") (write (internal-time/ticks->seconds run-time)) (newline) (display "GC time:\t") (write (internal-time/ticks->seconds gc-time)) (newline) (display "Actual time:\t") (write (internal-time/ticks->seconds real-time)) (newline))))))
Next step: ITERATION!