Каков реальный ответ на SICP 3.57?

SICP-упражнение 3.57. Сколько сложений выполняется, когда мы вычисляем nth число Фибоначчи, используя определение fibs на основе процедуры add-streams? Покажите, что количество дополнений было бы экспоненциально больше, если бы мы реализовали (delay ⟨exp⟩) просто как (lambda () ⟨exp⟩), не используя оптимизацию, обеспечиваемую процедурой memo-proc, описанной в 3.5.1.

В сети много решений. Большинство утверждает, что неоптимизированная версия последовательности memo-proc для последовательности fib аналогична вычислению обычной функции 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))}

Вот фактический код Racket:

#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. Прослеженный вывод рассказывает другую историю.


person nate    schedule 23.09.2019    source источник


Ответы (1)


[2021-05-05 Примечание. Это кардинально отличается от более ранней версии этого ответа 2019. Фактический результат тот же!]

Используя какую-то традиционную математическую нотацию вместо того, чтобы выражать все в схеме, поскольку мне кажется, что об этом легче думать, поток чисел Фибоначчи, f, выглядит следующим образом:

f = (0, 1, f(0) + f(1), f(1) + f(2), ..., f(n-1) + f(n-2), ...)

В этом выражении я расширил функцию add-streams очевидным образом.

Итак, теперь, если нет мемоизации, остается просто подсчитать количество сложений, участвующих в вычислении f(n). Что ж, количество дополнений — это количество добавлений в самом потоке + количество добавлений в двух добавляемых нами составных потоках.

  • количество добавлений в самом потоке равно 0, если n <= 1 в противном случае n - 1, что вы можете увидеть, просто взглянув на поток выше и подсчитав символы '+';
  • количество дополнений в потоках компонентов равно 0, если n <= 1 (нет потоков компонентов), иначе сумма количества добавлений для вычисления f(n-1) и < em>f(n-2).

Or:

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

  • количество добавлений в самом потоке, равное 0, если n <= 1 иначе n - 1, как и раньше;
  • плюс количество дополнений в потоках компонентов, которое равно нулю, поскольку они уже были вычислены.

Or:

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

что, очевидно, линейно по n.


Ниже приведен некоторый код Racket, который реализует достаточное количество потоков, чтобы быть опасным, с контролем запоминания над delay (называемым retard) и force (называемым advance), а также поддержкой подсчета вызовов: с этим вы можете легко эмпирически проверить приведенные выше результаты. fc вычисляет nth 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)))
person Community    schedule 26.09.2019
comment
Должен ли я рисовать диаграммы обработки сигналов, чтобы увидеть это более четко? - person nate; 27.09.2019
comment
@нат: я не знаю. Я думаю, что у настоящих специалистов по информатике должен быть какой-то подход к решению подобных проблем. В конце концов я просто ломаю голову над этим, делая предположения, а затем смотрю, согласуются ли они с фактическим подсчетом, что ужасно. - person ; 30.09.2019
comment
Прежде всего, спасибо, что уделили время и обдумали этот ответ. К сожалению, возвращаясь все это время спустя, я должен отказаться от ответа, потому что на самом деле нет никаких фактических доказательств того, что ваше уравнение верно. Мы просто продемонстрировали, что это кажется правильным. Поэтому SICP 3.57 остается здесь без ответа. Если вы или кто-то другой можете предоставить четкое доказательство уравнения и, надеюсь, схемы обработки сигналов, я приму ответ. Я могу вернуться к этому вопросу и попытаться ответить на него сам, так как это единственный вопрос в SICP, на который я не смог ответить удовлетворительно. - person nate; 01.05.2021
comment
@nate: Если вам нужно доказательство, а не просто ответ, вы, вероятно, должны были сказать это ... Но, очевидно, мой ответ не является доказательством: это просто ответ. - person ; 01.05.2021
comment
Хм. Вопрос заключается в следующем: сколько сложений выполняется, когда мы вычисляем n-е число Фибоначчи, используя определение вымыслов, основанное на процедуре добавления потоков? Покажите, что количество добавлений было бы экспоненциально больше, если бы мы реализовали (delay ‹exp›) просто как (lambda () ‹exp›), не используя оптимизацию, обеспечиваемую процедурой memo-proc, описанной в разделе 3.5.1.64. Показали ли мы, что количество сложений будет экспоненциально больше, если мы не докажем, что ваше уравнение для A(n) является правильным? - person nate; 01.05.2021
comment
Нет, мой ответ тот, когда delay не запоминается. Кажется, я ответил не на тот вопрос... - person ; 02.05.2021
comment
@nate Я делаю, чтобы переделать свой ответ, потому что я думаю, что теперь мне его легче увидеть. К сожалению, он будет совершенно другим, так что вы, возможно, захотите посмотреть его снова. - person ; 03.05.2021
comment
хм, хорошо. Кроме того, все, что мы сделали до сих пор, было связано с задержкой без запоминания, это был SICP 3.57, это был первоначальный вопрос, и именно на него вы ответили. - person nate; 03.05.2021