【问题标题】:Is there a tco pattern with two accumulating variables?是否存在具有两个累积变量的 tco 模式?
【发布时间】:2018-01-21 15:24:33
【问题描述】:

只是为了好玩(Project Euler #65)我想实现公式

n_k = a_k*n_k-1 + n_k-2

以一种有效的方式。 a_k1(* 2 (/ k 3)),取决于k

我从递归解决方案开始:

(defun numerator-of-convergence-for-e-rec (k)
  "Returns the Nth numerator of convergence for Euler's number e."
  (cond ((or (minusp k)) (zerop k) 0)
        ((= 1 k) 2)
        ((= 2 k) 3)
        ((zerop (mod k 3)) (+ (* 2 (/ k 3) (numerator-of-convergence-for-e-rec (1- k)))
                              (numerator-of-convergence-for-e-rec (- k 2))))
        (t (+ (numerator-of-convergence-for-e-rec (1- k))
              (numerator-of-convergence-for-e-rec (- k 2))))))

这适用于小型k,但对于k = 100 显然会很慢。

我不知道如何将此函数转换为可以进行尾调用优化的版本。我已经看到一个使用两个累加变量的模式 fibonacci numbers 但未能将此模式转换为我的函数。

是否有如何将复杂递归转换为 tco 版本的一般指南,或者我应该直接实施迭代解决方案。

【问题讨论】:

  • 在这种情况下,简单地记忆函数可能是最简单的解决方案。
  • 我在想它,但从未在现实生活中使用过这种技术。有这方面的好图书馆吗?
  • Fare-memoization 应该可以工作。或者只是手动将结果存储到一个向量中,并在后续调用中使用相同的K 从那里查找它们。

标签: recursion common-lisp tail-call-optimization


【解决方案1】:

首先,请注意,记忆化可能是优化代码的最简单方法:它不会反转操作流程;你用给定的 k 调用你的函数,它会回到零来计算以前的值,但是有一个缓存。但是,如果您想使用 TCO 将函数从递归转换为迭代,则必须计算从零到 k 的内容,并假设您有一个恒定大小的堆栈/内存。

阶梯函数

首先,写一个函数来计算当前的n给定kn-1n-2

(defun n (k n1 n2)
  (if (plusp k)
      (case k
        (1 2)
        (2 3)
        (t (multiple-value-bind (quotient remainder) (floor k 3)
             (if (zerop remainder)
                 (+ (* 2 quotient n1) n2)
                 (+ n1 n2)))))
      0))

这一步应该很简单;在这里,我稍微重写了你的函数,但实际上我只提取了计算 n 的部分,给出了前面的 nk

具有递归(迭代)调用的修改函数

现在,您需要从 k 开始调用 n,从 0 到您要计算的最大值,以下命名为 m。因此,我将添加一个参数m,它控制递归调用何时停止,并使用修改后的参数递归调用n。可以看到参数发生了变化,当前的n1 是下一个n2,等等

(defun f (m k n1 n2)
  (if (< m k)
      n1
      (if (plusp k)
        (case k
          (1 (f m (1+ k) 2 n1))
          (2 (f m (1+ k) 3 n1))
          (t (multiple-value-bind (quotient remainder) (floor k 3)
           (if (zerop remainder)
             (f m (1+ k) (+ (* 2 quotient n1) n2) n1)
             (f m (1+ k) (+ n1 n2) n1)))))
        (f m (1+ k) 0 n1))))

仅此而已,只是您不想向用户显示此界面。实际函数 g 正确引导对 f 的初始调用:

(defun g (m)
  (f m 0 0 0))

此函数的跟踪呈现箭头“>”形状,尾递归函数就是这种情况(跟踪可能会抑制尾调用优化):

  0: (G 5)
    1: (F 5 0 0 0)
      2: (F 5 1 0 0)
        3: (F 5 2 2 0)
          4: (F 5 3 3 2)
            5: (F 5 4 8 3)
              6: (F 5 5 11 8)
                7: (F 5 6 19 11)
                7: F returned 19
              6: F returned 19
            5: F returned 19
          4: F returned 19
        3: F returned 19
      2: F returned 19
    1: F returned 19
  0: G returned 19
19

带循环的驱动函数

当我们在原始函数n 中注入尾递归调用时,可能会有点困难,或者使您的代码难以阅读。我认为最好使用循环,因为:

  1. 与尾递归调用不同,您可以保证代码按照您的意愿运行,而不必担心您的实现是否会真正优化尾调用。
  2. step 函数n 的代码更简单,只表达发生了什么,而不是详细说明如何(尾递归调用在这里只是一个实现细节)。

通过上面的函数n,可以将g改成:

(defun g (m)
  (loop
     for k from 0 to m
     for n2 = 0 then n1
     for n1 = 0 then n
     for n = (n k n1 n2)
     finally (return n)))

是否有一个通用指南如何将复杂的递归转换为 tco 版本还是应该直接实现迭代解决方案?

找到一个阶梯函数,将计算从基本情况推进到一般情况,并将中间变量作为参数,特别是过去调用的结果。该函数可以调用自身(在这种情况下,它将是尾递归的,因为您必须先计算所有参数),或者简单地在循环中调用。计算初始值时必须小心,与使用简单的递归函数相比,您可能会遇到更多极端情况。

 另见

方案的named let、Common Lisp 中的RECUR 宏和Clojure 中的recur 特殊形式。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-06
    相关资源
    最近更新 更多