SICP 练习 3.57:当我们使用基于
add-streams
过程的fibs
定义计算第n个斐波那契数时,执行了多少次加法?表明如果我们简单地实现(delay ⟨exp⟩)
(lambda () ⟨exp⟩)
,而不使用 3.5.1 中描述的memo-proc
过程提供的优化,则添加的数量将呈指数级增长。
网上有很多解决方案。大多数人声称fib
序列的非优化memo-proc
序列版本与计算非记忆正则fib
函数相同。在跟踪未优化memo-proc
版本的添加时,我看到了不同的故事。
设 A(n( 为(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
在非优化(非记忆流(上使用替换和函数定义时,我可以确切地看到这些添加是什么以及它们发生的原因,但我很难想出一个好的方程来回答它实际上是指数的问题。
例如,A(4( 的添加项是:
- 1 + 0
- 1 + 0
- 1 + 1
- 1 + 0
- 1 + 1
- 1 + 0
- 2 + 1
这里有一些伪代码来显示(stream-ref fibs 4)
的替换,其中'.
'代表中缀stream-cons
,{e}
代表执行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))}
这是实际的球拍代码:
#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)
网上的大多数答案都说a(n) = a(n - 1) + a(n - 2) + 1
。 跟踪的输出讲述了一个不同的故事。
[2021-05-05注意:这与本答案的早期 2019版本有很大不同。 实际结果是一样的!
使用某种传统的数学符号而不是在 Scheme 中表达所有内容,因为我发现更容易思考,斐波那契数流,f
,看起来像这样:
f = (0, 1, f(0) + f(1), f(1) + f(2), ..., f(n-1) + f(n-2), ...)
在这个表达式中,我以明显的方式扩展了add-streams
函数。
所以现在,如果没有记忆,只需计算计算f(n)
所涉及的添加数量即可。 那么,添加的数量是流本身的添加数量+我们正在添加的两个组件流中的添加数量。
- 如果
n <= 1
其他n - 1
,则流本身的添加数量是0
的,你可以从上面的流中看出,并计算'+
'符号; - 如果
n <= 1
(无组件流(,则组件流中的添加数0
计算f(n-1)
和f(n-2)
的添加数之和。
或:
a = (0, 0, 1 + a(0) + a(1), 2 + a(1) + a(2), ..., n-1 + a(n-1) + a(n-2), ...)
当然,这在n
是指数级的。 检测代码以计算添加次数并编写此a
函数以检查它们是否同意很容易,他们确实这样做了。
我发现很难推理f
被记住的情况(这实际上意味着force
在记忆时(,因为所有黑暗的角落都隐藏着状态。 但我认为,诀窍是要记住流是线性访问的:要计算f(n)
我必须已经计算了f(n-1)
。 一旦完成,那么再次计算它是一个查找:不涉及添加。 所以这次a
是
- 流本身的添加数量,如果像以前一样
n - 1
n <= 1
0
; - 加上组件流中的添加数,该数为零,因为它们已经计算出来。
或:
a = (0, 0, 1, 2, ..., n-1, ...)
这显然是线性的n
。
下面是一些Racket代码,它实现了足够多的危险流,对delay
(称为retard
(和force
(称为advance
(进行记忆控制,以及呼叫计数支持:有了这个,您可以轻松地凭经验检查上述结果。fc
计算第n
FIB 并计算对+
的调用,a
和b
是上述a
的非记忆和记忆版本。
#lang racket
;;;; advance and retard are force & delay
;;; memoization can be controlled
;;;
(define advance-memoizes? (make-parameter #t))
(define not-memoized (cons #f #f))
(define-syntax-rule (retard form)
(let ([memo not-memoized])
(thunk
(if (advance-memoizes?)
(begin
(when (eq? memo not-memoized)
(set! memo form))
memo)
form))))
(define (advance retarded)
(retarded))
;;;; mλ is a restricted memoizing λ
;;; Again memoization can be controlled
;;;
(define mλ-memoizes? (make-parameter #t))
(define-syntax-rule (mλ (arg) form)
(let ([memos (make-hash)])
(λ (arg)
(if (mλ-memoizes?)
(hash-ref! memos arg (thunk form))
form))))
;;;; Streams
;;; functions are prefixed with s
(define-values (snull snull?)
(values '() null?))
(define-syntax-rule (scons this that)
(cons this (retard that)))
(define scar car)
(define (scdr stream)
(advance (cdr stream)))
(define (sref s n)
(if (= n 0)
(scar s)
(sref (scdr s) (- n 1))))
(define (smap p . streams)
(let smap* ([ss streams])
(if (memf snull? ss)
snull
(scons
(apply p (map scar ss))
(smap* (map scdr ss))))))
;;;; Counting function calls
;;;
(define (call/counted f . gs)
;; call f with 2 arguments for each function in gs:
;; - a function which is equivalent to the element of g
;; - and a function which will return the call count of that function.
;; Recursive calls to the gs are not counted
(let cc-loop ([gt gs]
[fs '()])
(match gt
['() (apply f (reverse fs))]
[(cons g gtt)
(let ([gc 0])
(cc-loop gtt (list*
(thunk gc)
(λ args
(set! gc (+ gc 1))
(apply g args))
fs)))])))
;;;; Counting fibs
;;;
(define (fc n #:memoize? (memoize? #t))
;; Return nth fib and number of calls to +
(parameterize ([advance-memoizes? memoize?])
(call/counted
(λ (+/counted +-count)
(define fibs
(scons 0
(scons 1
(smap +/counted (scdr fibs)
fibs))))
(values (sref fibs n)
(+-count)))
+)))
(define a
;; unmemoized count (but this needs to be memoized!)
(mλ (m)
(cond
[(or (= m 0) (= m 1)) 0]
[(> m 1)
(+ (- m 1)
(a (- m 1))
(a (- m 2)))]
[else
(error 'a "negative creep")])))
(define (b m)
;; memoized count
(floor (- m 1)))