Подтвердить что ты не робот

Как мне заменить рекурсивную функцию в Lisp?

Я новичок Lisp. Я пытаюсь memoize рекурсивную функцию для вычисления количества терминов в последовательности Collatz (для проблемы 14 в Project Euler). Мой код на данный момент:

(defun collatz-steps (n)
  (if (= 1 n) 0
       (if (evenp n) 
           (1+ (collatz-steps (/ n 2)))
           (1+ (collatz-steps (1+ (* 3 n)))))))

(defun p14 ()
  (defvar m-collatz-steps (memoize #'collatz-steps))
  (let 
      ((maxsteps (funcall m-collatz-steps 2))
       (n 2)
       (steps))
    (loop for i from 1 to 1000000
          do 
          (setq steps (funcall m-collatz-steps i))
          (cond 
            ((> steps maxsteps) 
             (setq maxsteps steps)
             (setq n i))
            (t ())))
    n))


(defun memoize (fn)
  (let ((cache (make-hash-table :test #'equal)))
    #'(lambda (&rest args)
        (multiple-value-bind 
              (result exists)
            (gethash args cache)
          (if exists
              result
              (setf (gethash args cache)
                    (apply fn args)))))))

Функция memoize такая же, как и в Вкл. Lisp.

Этот код фактически не дает ускорения по сравнению с не memoized версией. Я считаю, что это связано с рекурсивными вызовами, вызывающими не-мемуаризованную версию функции, которая вроде бы поражает цель. В этом случае, каков правильный способ сделать меморандум здесь? Есть ли способ, чтобы все вызовы исходной функции вызывали сама memoized, устраняя необходимость в специальном символе шага m-collatz?

EDIT: Исправлен код для

(defvar m-collatz-steps (memoize #'collatz-steps))

что я имел в своем коде. Перед редактированием я ошибочно поставил:

(defvar collatz-steps (memoize #'collatz-steps))

Увидев эту ошибку, я дал еще одну идею, и я попытался использовать этот последний defvar и изменить рекурсивные вызовы на

       (1+ (funcall collatz-steps (/ n 2)))
       (1+ (funcall collatz-steps (1+ (* 3 n))))

Кажется, что он выполняет memoization (ускорение от 60 секунд до 1,5 секунд), но требует изменения оригинальной функции. Есть ли более чистое решение, которое не связано с изменением исходной функции?

4b9b3361

Ответ 1

Я предполагаю, что вы используете Common- Lisp, у которого есть отдельные пространства имен для имен переменных и функций. Чтобы memoize функцию, названную символом, вам необходимо изменить ее привязку к функциям с помощью функции fdefinition:

(setf (fdefinition 'collatz-steps) (memoize #'collatz-steps))

(defun p14 ()
  (let ((mx 0) (my 0))
    (loop for x from 1 to 1000000
          for y = (collatz-steps x)
          when (< my y) do (setf my y mx x))
    mx))

Ответ 2

что-то вроде этого:

(setf collatz-steps (memoize lambda (n)
  (if (= 1 n) 0
    (if (evenp n) 
        (1+ (collatz-steps (/ n 2)))
        (1+ (collatz-steps (1+ (* 3 n))))))))

IOW: ваша оригинальная (не memoized) функция анонимна, и вы даете только имя для его memoizing.

Ответ 3

Вот функция memoize, которая возвращает функцию символа:

(defun memoize-function (function-name)
  (setf (symbol-function function-name)
    (let ((cache (make-hash-table :test #'equal)))
         #'(lambda (&rest args)
             (multiple-value-bind 
                 (result exists)
                (gethash args cache)
               (if exists
                   result
                   (setf (gethash args cache)
                         (apply fn args)))))))

Затем вы сделали бы что-то вроде этого:

(defun collatz-steps (n)
  (if (= 1 n) 0
      (if (evenp n) 
          (1+ (collatz-steps (/ n 2)))
          (1+ (collatz-steps (1+ (* 3 n)))))))

(memoize-function 'collatz-steps)

Я оставлю это для вас, чтобы сделать функцию unmemoize.

Ответ 4

Обратите внимание на несколько вещей:

(defun foo (bar)
   ... (foo 3) ...)

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

В Common Lisp компилятор файлов может предположить, что FOO не изменяется. Он НЕ будет вызывать обновленный FOO позже. Если вы измените привязку функции FOO, тогда вызов исходной функции по-прежнему будет идти к старой функции.

Так что память в саморекурсивной функции НЕ будет работать в общем случае. Особенно если вы используете хороший компилятор.

Вы можете обойти это, чтобы всегда проходить через символ: (funcall 'foo 3)

(DEFVAR...) - форма верхнего уровня. Не используйте его внутри функций. Если вы объявили переменную, установите ее позже с помощью SETQ или SETF.

Для вашей проблемы я бы просто использовал хеш-таблицу для хранения промежуточных результатов.

Ответ 5

Эта функция точно соответствует тому, что Питер Норвиг дает в качестве примера функцию, которая кажется хорошим кандидатом на мемонирование, но это не так.

См. рисунок 3 (функция "Hailstone" ) его оригинальной статьи по мемуаризации ( "Использование автоматической напоминания в качестве инструмента разработки программного обеспечения в реальных системах AI" ).

Итак, я предполагаю, что даже если вы заработаете механику работы с memoization, в этом случае это не ускорит ее.

Ответ 6

Изменение "оригинальной" функции необходимо, потому что, как вы говорите, нет другого способа для рекурсивного вызова (-ов), чтобы быть обновленным для вызова memoized версии.

К счастью, способ lisp заключается в том, чтобы найти функцию по имени каждый раз, когда ее нужно вызвать. Это означает, что достаточно заменить привязку функции memoized версией функции, так что рекурсивные вызовы будут автоматически искать и возвращаться через memoization.

huaiyuan code показывает ключевой шаг:

(setf (fdefinition 'collatz-steps) (memoize #'collatz-steps))

Этот трюк также работает в Perl. Однако на языке, подобном C, memoized версия функции должна быть закодирована отдельно.

Некоторые реализации lisp предоставляют систему под названием "совет", которая предоставляет стандартизованную структуру для замены функций с помощью расширенных версий. В дополнение к функциональным обновлениям, таким как memoization, это может быть чрезвычайно полезно при отладке, вставляя отладочные отпечатки (или полностью останавливаясь и предоставляя продолжение) без изменения исходного кода.

Ответ 7

Некоторое время назад я написал небольшую процедуру memoization для Схемы, которая использовала цепочку замыканий для отслеживания сохраненного состояния:

(define (memoize op)
  (letrec ((get (lambda (key) (list #f)))
           (set (lambda (key item)
                  (let ((old-get get))
                    (set! get (lambda (new-key)
                                (if (equal? key new-key) (cons #t item)
                                    (old-get new-key))))))))
    (lambda args
      (let ((ans (get args)))
        (if (car ans) (cdr ans)
            (let ((new-ans (apply op args)))
              (set args new-ans)
              new-ans))))))

Это нужно использовать так:

(define fib (memoize (lambda (x)
                       (if (< x 2) x
                           (+ (fib (- x 1)) (fib (- x 2)))))))

Я уверен, что это можно с легкостью портировать на ваш любимый лексически ограниченный Lisp вкус.

Ответ 8

Я бы, наверное, сделал что-то вроде:

(let ((memo (make-hash-table :test #'equal)))
  (defun collatz-steps (n)
    (or (gethash n memo)
    (setf (gethash n memo)
          (cond ((= n 1) 0)
            ((oddp n) (1+ (collatz-steps (+ 1 n n n))))
            (t (1+ (collatz-steps (/ n 2)))))))))

Это нехорошо и функционально, но тогда это не так много хлопот, и это действительно работает. Недостатком является то, что вы не получаете удобную версию без исправления для проверки и очистки кеша, граничащего с "очень сложным".