我是Lisp的初学者。我正在尝试记忆递归函数来计算Collatz sequence中的术语数(对于Project Euler中的问题14)。我的代码是:
(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功能与On Lisp书中给出的功能相同。
与非记忆版本相比,此代码实际上没有提供任何加速。我相信这是由于调用函数的非memoized版本的递归调用,这有点违背了目的。在这种情况下,这里进行记忆的正确方法是什么?是否有任何方法可以调用原始函数调用memoized版本本身,无需使用特殊的m-collatz-steps符号?
编辑:更正了代码
(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秒),但需要更改原始功能。有没有更清洁的解决方案,不涉及改变原有的功能?
答案 0 :(得分:10)
我假设您正在使用Common-Lisp,它具有用于变量和函数名称的单独名称空间。为了记住符号命名的函数,你需要通过访问器`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))
答案 1 :(得分: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:您的原始(非记忆)功能是匿名的,您只能为记忆它的结果命名。
答案 2 :(得分:1)
这是一个重新绑定符号函数的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功能。
答案 3 :(得分:1)
更改“原始”功能是必要的,因为正如您所说,没有其他方法可以更新递归调用以调用已记忆的版本。
幸运的是,lisp的工作方式是每次需要调用时按名称找到函数。这意味着用函数的memoized版本替换函数绑定就足够了,这样递归调用将自动查找并重新进入memoization。
怀远的代码显示了关键步骤:(setf (fdefinition 'collatz-steps) (memoize #'collatz-steps))
这个技巧也适用于Perl。但是,在像C这样的语言中,函数的memoized版本必须单独编码。
一些lisp实现提供了一个名为“advice”的系统,它提供了一个标准化的结构,用于将功能替换为自身的增强版本。除了memoization等功能升级之外,通过插入调试打印(或完全停止并提供可持续的提示)而不修改原始代码,这在调试中非常有用。
答案 4 :(得分:1)
请注意以下几点:
(defun foo (bar)
... (foo 3) ...)
上面是一个自己调用的函数。
在Common Lisp中,文件编译器可以假设FOO不会改变。它不会在以后调用更新的FOO。如果更改FOO的功能绑定,则原始功能的调用仍将转到旧功能。
所以记住自递归函数在一般情况下不起作用。如果您使用的是良好的编译器,尤其如此。
你可以解决它总是通过符号例如:(funcall'foo 3)
(DEFVAR ...)是一种顶级形式。不要在功能内部使用它。如果已声明变量,请稍后使用SETQ或SETF进行设置。
对于您的问题,我只使用哈希表来存储中间结果。
答案 5 :(得分:1)
这个函数正是Peter Norvig给出的一个函数的例子,它似乎是一个很好的候选记忆,但事实并非如此。
参见关于记忆的原始论文(“使用自动记忆作为真实AI系统中的软件工程工具”)的图3(“Hailstone”功能)。
所以我猜,即使你得到了memoization工作的机制,在这种情况下它也不会真正加速它。
答案 6 :(得分:0)
前段时间我为Scheme编写了一个小的memoization例程,它使用一系列闭包来跟踪memoized状态:
(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风格。
答案 7 :(得分:0)
我可能会做类似的事情:
(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)))))))))
这不是很好和功能,但是,它没有太多的麻烦,它确实有效。缺点是你没有得到一个方便的unmemoized版本来测试和清除缓存接近“非常困难”。