如何在 Lisp 中记忆递归函数?
Posted
技术标签:
【中文标题】如何在 Lisp 中记忆递归函数?【英文标题】:How do I memoize a recursive function in Lisp? 【发布时间】:2010-09-20 08:49:54 【问题描述】:我是 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 书中给出的功能相同。
与非记忆版本相比,此代码实际上并没有提供任何加速。我相信这是由于递归调用调用了函数的非记忆版本,这有点违背了目的。在那种情况下,在这里进行记忆的正确方法是什么?有没有办法让对原始函数的所有调用都调用记忆化版本本身,从而消除对特殊 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))))
这似乎确实执行了记忆(从大约 60 秒加速到 1.5 秒),但需要更改原始功能。是否有不涉及更改原始功能的更清洁的解决方案?
【问题讨论】:
【参考方案1】:类似这样的:
(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:你的原始(非记忆)函数是匿名的,你只给记忆它的结果一个名字。
【讨论】:
是的,这样更清楚,但我认为你应该使用 defun 宏: (defun collatz-steps (n) (memoize #'(lambda (x) etc. ... n) )【参考方案2】:我假设您使用的是 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))
【讨论】:
如果你在函数定义之后调用 setf 就可以了【参考方案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 函数。
【讨论】:
问题:递归调用一般不会通过设置符号函数来改变。 你的代码有以下问题:1.未定义变量:FN 2.缺少')' 此代码不起作用(fn
未绑定)。当然,一旦修复,很高兴恢复为 +1。【参考方案4】:
更改“原始”函数是必要的,因为正如您所说,没有其他方法可以更新递归调用以调用记忆化版本。
幸运的是,lisp 的工作方式是在每次需要调用时按名称 查找函数。这意味着用函数的memoized版本替换函数绑定就足够了,这样递归调用会自动查找并通过memoization重新进入。
怀远的代码显示关键步骤:
(setf (fdefinition 'collatz-steps) (memoize #'collatz-steps))
这个技巧也适用于 Perl。然而,在像 C 这样的语言中,函数的记忆版本必须单独编码。
一些 lisp 实现提供了一个称为“advice”的系统,该系统提供了一个标准化的结构,用于将函数替换为自身的增强版本。除了像记忆这样的功能升级之外,这在调试中非常有用,通过插入调试打印(或完全停止并给出可继续的提示)而不修改原始代码。
【讨论】:
不,Common Lisp 不会每次都按名称查找函数。一个好的编译器会生成直接调用函数的代码。这也在 ANSI CL 标准中有所描述。【参考方案5】:不久前,我为 Scheme 编写了一个小记忆例程,它使用闭包链来跟踪记忆状态:
(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 风格中。
【讨论】:
【参考方案6】:我可能会这样做:
(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)))))))))
它既不美观也不实用,但是,它并不麻烦,而且确实有效。缺点是你没有一个方便的非记忆版本来测试,清除缓存几乎“非常困难”。
【讨论】:
【参考方案7】:注意几点:
(defun foo (bar)
... (foo 3) ...)
上面是一个调用自身的函数。
在 Common Lisp 中,文件编译器可以假设 FOO 不会改变。稍后它不会调用更新的 FOO。如果改变了 FOO 的函数绑定,那么原函数的调用还是会转到旧函数。
所以记忆自递归函数在一般情况下是行不通的。尤其是如果您使用的是好的编译器。
您可以解决它以始终通过符号,例如:(funcall 'foo 3)
(DEFVAR ...) 是一种***形式。不要在函数内部使用它。如果您声明了一个变量,请稍后使用 SETQ 或 SETF 设置它。
对于您的问题,我只是使用哈希表来存储中间结果。
【讨论】:
【参考方案8】:这个函数正是 Peter Norvig 给出的一个函数示例,它似乎是一个很好的记忆候选函数,但事实并非如此。
参见他关于记忆化的原始论文(“在现实世界人工智能系统中使用自动记忆化作为软件工程工具”)的图 3(函数“Hailstone”)。
所以我猜,即使你让记忆机制发挥作用,在这种情况下也不会真正加快速度。
【讨论】:
以上是关于如何在 Lisp 中记忆递归函数?的主要内容,如果未能解决你的问题,请参考以下文章