SICP Exercise 3.57: How many additions are performed when we compute the nth Fibonacci number using the definition of
fibs
based on theadd-streams
procedure? Show that the number of additions would be exponentially greater if we had implemented(delay ⟨exp⟩)
simply as(lambda () ⟨exp⟩)
, without using the optimization provided by thememo-proc
procedure described in 3.5.1.
There are many solutions online. Most claim that the non-optimized memo-proc
sequence version of the fib
sequence is the same as computing the non-memoized regular fib
function. When tracing additions for the non-optimized memo-proc
version, I see a different story.
Let A(n) be the number of additions performed for (stream-ref fibs n)
- A(0) = 0
- A(1) = 0
- A(2) = 1
- A(3) = 3
- A(4) = 7
- A(5) = 14
- A(6) = 26
When using substitution and the function definitions on the non-optimized (non-memoized stream) I can see exactly what these additions are and why they are occurring but I am having trouble coming up with a good equation to answer the question that it actually is exponential.
The additions traced for A(4), for example, are:
- 1 + 0
- 1 + 0
- 1 + 1
- 1 + 0
- 1 + 1
- 1 + 0
- 2 + 1
Here is some pseudocode to show the substitutions for (stream-ref fibs 4)
, where '.
' represents infix stream-cons
and {e}
represents promise to execute e
.
(cddddr fibs)
(cddr (add-streams (cdr fibs) fibs))
(cddr (stream-map + (cdr fibs) fibs)))
(cddr ((+ 1 0) . {stream-map + (cddr fibs) (cdr fibs)}))
(cdr (stream-map + (cddr fibs) (cdr fibs)))
(cdr (stream-map + ((+ 1 0) . {stream-map + (cddr fibs (cdr fibs)}) (cdr fibs))
(cdr (+ 1 1) . {stream-map + (stream-map + (cddr fibs) (cdr fibs)) (cddr fibs)})
(stream-map + (stream-map + (cddr fibs) (cdr fibs)) (cddr fibs))
(stream-map + (stream-map + ((+ 1 0) . {stream-map + (cddr fibs) (cdr fibs)}) (cdr fibs)) (cddr fibs)
(stream-map + (stream-map + ((+ 1 0) . {stream-map + (cddr fibs) (cdr fibs)}) (1 . {stream-map + (cdr fibs) fibs)})) (cddr fibs))
(stream-map + ((+ 1 1) . {stream-map + (stream-map + (cddr fibs) (cdr fibs)) (stream-map + (cdr fibs) fibs)}) ((+ 1 0) . {stream-map + (cddr fibs) (cdr fibs)})
(+ 2 1) . {stream-map + (stream-map + (stream-map + (cddr fibs) (cdr fibs)) (stream-map + (cdr fibs) fibs))) (stream-map + (cddr fibs) (cdr fibs))}
Here is the actual Racket code:
#lang racket
(define-syntax-rule (delay f) (lambda () f))
(define (force f) (f))
(define stream-null? null?)
(define the-empty-stream '())
(define-syntax-rule (cons-stream a b)
(cons a (delay b)))
(define stream-car car)
(define (stream-cdr stream) (force (cdr stream)))
(define (add-streams s1 s2)
(define (add x y)
(begin
(display "Adding ")
(display x)
(display " + ")
(display y)
(newline)
(+ x y)))
(stream-map add s1 s2))
(define (stream-map proc . argstreams)
(if (stream-null? (car argstreams))
the-empty-stream
(cons-stream
(apply proc (map stream-car argstreams))
(apply stream-map
(cons proc
(map stream-cdr
argstreams))))))
(define (stream-ref s n)
(if (= n 0)
(stream-car s)
(stream-ref (stream-cdr s) (- n 1))))
(define fibs
(cons-stream
0 (cons-stream
1 (add-streams
(stream-cdr fibs) fibs))))
(stream-ref fibs 4)
Most answers online say something like a(n) = a(n - 1) + a(n - 2) + 1
.
The traced output tells a different story.