【问题标题】:What is the real answer to SICP 3.57SICP 3.57的真正答案是什么
【发布时间】:2020-01-23 20:07:05
【问题描述】:

SICP 练习 3.57:当我们使用 fibs 的定义计算 nth 斐波那契数时,执行了多少次加法运算add-streams 程序?表明如果我们将(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。 跟踪的输出讲述了一个不同的故事。

【问题讨论】:

    标签: scheme racket fibonacci sicp lazy-sequences


    【解决方案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 函数。

    所以现在,如果没有 memoization,只需计算计算中涉及的加法数量即可f(n)。好吧,添加的数量是流本身中的添加数量 + 我们要添加的两个组件流中的添加数量。

    • 如果n <= 1,则流本身的加法数为0,否则为n - 1,你可以从中看到只需查看上面的流并计算“+”符号;
    • 如果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则为0,否则n - 1如前;
    • 加上组件流中添加的数量,为零,因为它们已经被计算过了。

    或者:

    a = (0, 0, 1, 2, ..., n-1, ...)

    这在n中显然是线性的。


    下面是一些 Racket 代码,它实现了足够危险的流,具有对 delay(称为 retard)和 force(称为 advance)的记忆控制,以及呼叫计数支持:这样您就可以轻松地凭经验检查上述结果。 fc 计算 nth fib 并计算对 +ab 的调用是上述 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)))
    

    【讨论】:

    • 我应该画信号处理图来更清楚地看到这一点吗?
    • @nate:我不知道。我认为适当的计算机科学人员必须有一些方法来处理这样的问题。我最终只是把它弄糊涂了,做出猜测,然后看看他们是否与实际计数一致,这太可怕了。
    • 首先感谢您花时间和精力来回答这个问题。不幸的是,后来一直回来我不得不不接受答案,因为实际上没有实际证据证明您的方程式是正确的。我们已经简单地证明了这似乎是正确的。因此 SICP 3.57 在这里仍然没有答案。如果您或其他人可以提供等式的明确证明并希望信号处理图我会接受答案。我可能会回到这个问题并尝试自己回答,因为这是 SICP 中唯一我无法满意地回答的问题。
    • @nate:如果你想要一个证明而不仅仅是答案,你可能应该这么说......但显然我的答案不是证明:它只是答案。
    • 嗯。问题是这样的:“当我们使用基于 add-streams 过程的 fibs 定义计算第 n 个斐波那契数时,执行了多少次加法?表明如果我们实现了(延迟 ) 简单地作为 (lambda () ),而不使用第 3.5.1.64 节中描述的 memo-proc 过程提供的优化”。如果我们没有证明你的 A(n) 方程是正确的,我们是否证明了加法的数量会成倍增加?
    猜你喜欢
    • 2022-11-13
    • 1970-01-01
    • 2018-07-07
    • 1970-01-01
    • 1970-01-01
    • 2013-09-02
    • 2022-06-16
    • 2013-05-26
    • 1970-01-01
    相关资源
    最近更新 更多