Производительность запоминания - упражнение SICP 3.27 кажется неправильным - PullRequest
0 голосов
/ 19 октября 2018

Я обнаружил фактическую ошибку в книге SICP?В нем говорится:

Упражнение 3.27. Запоминание (также называемое табулированием) - это метод, позволяющий процедуре записать в локальной таблице значения, которые ранее были вычислены.Этот метод может существенно повлиять на производительность программы.Записанная процедура поддерживает таблицу, в которой хранятся значения предыдущих вызовов, используя в качестве ключей аргументы, которые выдали значения.Когда запрошенную процедуру просят вычислить значение, она сначала проверяет таблицу, чтобы увидеть, существует ли уже значение, и, если это так, просто возвращает это значение.В противном случае он вычисляет новое значение обычным способом и сохраняет его в таблице.В качестве примера запоминания напомним из 1.2.2 показательный процесс для вычисления чисел Фибоначчи:

(define (fib n)
  (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1))
                 (fib (- n 2))))))

Запомнившаяся версия той же процедуры -

(define memo-fib
  (memoize 
   (lambda (n)
     (cond ((= n 0) 0)
           ((= n 1) 1)
           (else 
            (+ (memo-fib (- n 1))
               (memo-fib (- n 2))))))))

, где памятник определен как

(define (memoize f)
  (let ((table (make-table)))
    (lambda (x)
      (let ((previously-computed-result 
             (lookup x table)))
        (or previously-computed-result
            (let ((result (f x)))
              (insert! x result table)
              result))))))

и затем он говорит:

Объясните, почему memo-fib вычисляет n-й Фибоначчичисло в количестве шагов, пропорциональное N.

Процедуры insert! и lookup определены в книге следующим образом:

(define (lookup key table)
  (let ((record (assoc key (cdr table))))
    (if record
        (cdr record)
        false)))

(define (assoc key records)
  (cond ((null? records) false)
        ((equal? key (caar records)) 
         (car records))
        (else (assoc key (cdr records)))))

(define (insert! key value table)
  (let ((record (assoc key (cdr table))))
    (if record
        (set-cdr! record value)
        (set-cdr! table
                  (cons (cons key value) 
                        (cdr table)))))
  'ok)

Теперь,assoc имеет количество шагов, пропорциональное n.И поскольку lookup и insert! используют assoc, они оба имеют число шагов, пропорциональное N .

Я не понимаю, как memo-fibимеет количество шагов, пропорциональных N .Мои наблюдения таковы:

  • Из-за определения аргумента для memo-fib (лямбда-выражение, которое имеет n в качестве формального параметра), таблица будет иметь в основном упорядоченные ключи, а ключи будутбыть упорядоченным.Поэтому можно предположить, что любой вызов для поиска будет близок к операции с постоянным временем.
  • Insert!, с другой стороны, не будет знать, что ключи будут добавлены в некотором порядке.Если значение не существует в таблице, insert! всегда будет сканировать весь список, поэтому каждый раз будет иметь число шагов, пропорциональное n.
  • Если у нас есть n-1элементов в таблице, и мы хотим вычислить (memo-fib n), он будет иметь количество шагов, пропорциональное n из-за assoc в insert!.
  • Если у нас нет ключей,тогда (memo-fib n) будет иметь количество шагов, пропорциональное n 2 , поскольку insert! вызывается при каждом рекурсивном вызове memo-fib.

Еслиlookup и insert! являются постоянными, тогда было бы целесообразно, чтобы memo-fib имел количество шагов, пропорциональное n .Но реальное количество шагов выглядит следующим образом: n * (nk) , где k - это количество ключей, уже находящихся в таблице .

Am.Я делаю это неправильно?Чего мне не хватает?

1 Ответ

0 голосов
/ 25 октября 2018

Похоже, ты был прав.Он имеет примерно квадратичную «сложность», эмпирически .assoc в insert! не нужен вообще;удаление его не меняет возвращаемого значения и только делает его намного более быстрым.

Чтобы сделать тесты более чистыми, я изменил памятку на , а не , чтобы разделить таблицу между вызовами.

#lang r5rs

(#%require srfi/19)

(define false #f)
(define true #t)

(define (memoize f)
   (let ((table (make-table)))
    (lambda (x)
      (let ((previously-computed-result 
             (lookup x table)))
        (or previously-computed-result
            (let ((result (f x)))
              (insert! x result table)
              result))))))

(define (lookup key table)
  (let ((record (assoc key (cdr table))))
    (if record
        (cdr record)
        false)))

(define (assoc key records)
  (cond ((null? records) false)
        ((equal? key (caar records)) 
         (car records))
        (else (assoc key (cdr records)))))

(define (insert! key value table)
  (let ((record #f                                 ; NB
                ; (assoc key (cdr table))          ; NB
                ))
    (if record
        (set-cdr! record value)
        (set-cdr! table
                  (cons (cons key value) 
                        (cdr table)))))
  'ok)

(define (make-table)
   (list '*table*))

(define memo-fib      
  (lambda (n)
    (letrec ((mf (memoize                          ; NB
                  (lambda (n)
                    (cond ((= n 0) 0)
                          ((= n 1) 1)
                          (else 
                           (+ (mf (- n 1))
                              (mf (- n 2)))))))))
      (mf n))))

(define (tt n)
  (let* ((t1 (current-time))
         (f  (memo-fib n))
         (t2 (current-time))
         (td (time-difference t2 t1))
         (n  (time-nanosecond td)))
    (/
       (+ (* (time-second td) 1000000000)
          n)
       1000000.0)))   ; time in milliseconds

; > (memo-fib 100)
; 354224848179261915075

(define (tt2 n1 n2)
  (let* ((t1 (tt n1))
         (t2 (tt n2)))
    (values t1 t2
            (cond ((> t1 0)
                   (/ (log (/ t2 t1)) (log (/ n2 n1))))))))

Тестирование проводится очень элементарно.Время указывается в миллисекундах.

; with the lookup in insert!:
;         n1   n2        t1     t2       a  in t ~ n^a, empirically
; > (tt2 2000 3000) ;=>  90.0  200.0  1.96936
; > (tt2 2000 3000) ;=> 100.0  220.0  1.94457
; > (tt2 2000 3000) ;=>  90.0  210.0  2.08969

; without the lookup: 80,000 takes under 1 second
; but run times are wildly erratic

Так что это действительно выглядит как упущение авторов, их использование общей процедуры insert!, когда на самом деле мы знаем , мы тольковставьте новые записи в таблицу - потому что мы запоминаем функцию на первом месте!

Итак, insert! следует заменить на insert-new!:

(define (memoize f)
   (let ((table (make-table)))
    (lambda (x)
      (let ((previously-computed-result 
             (lookup x table)))
        (or previously-computed-result
            (let ((result (f x)))
              (insert-new! x result table)
              result))))))

(define (insert-new! key value table)
  (set-cdr! table
                  (cons (cons key value) 
                        (cdr table)))
  'ok)

, а затем должно стать линейным.

...