【问题标题】:Can this function be simplified (made more "fast")?这个功能可以简化(变得更“快”)吗?
【发布时间】:2013-12-22 00:21:52
【问题描述】:

我想知道这是否是此功能的最快版本。

(defun foo (x y)
  (cond 
   ;if x = 0, return y+1
   ((zp x) (+ 1 y)) 
   ;if y = 0, return foo on decrement x and 1
   ((zp y) (foo (- x 1) 1)) 
   ;else run foo on decrement x and y = (foo x (- y 1))
   (t (foo (- x 1) (foo x (- y 1)))))) 

当我运行它时,我通常会遇到堆栈溢出错误,所以我试图找出一种不使用计算机来计算类似 (foo 3 1000000) 的方法。

通过分析函数,我认为它在递归情况下嵌入了 foo,导致 (foo 3 1000000) 中的溢出。但是,由于您正在减少 y,因此步数是否等于 y?

编辑:从 cmets 中删除谎言

【问题讨论】:

标签: scheme lisp computation-theory


【解决方案1】:

12 年前我写了这个:

(defun ackermann (m n)
  (declare (fixnum m n) (optimize (speed 3) (safety 0)))
  (let ((memo (make-hash-table :test #'equal))
        (ncal 0) (nhit 0))
    (labels ((ack (aa bb)
               (incf ncal)
               (cond ((zerop aa) (1+ bb))
                     ((= 1 aa) (+ 2 bb))
                     ((= 2 aa) (+ 3 (* 2 bb)))
                     ((= 3 aa) (- (ash 1 (+ 3 bb)) 3))
                     ((let* ((key (cons aa bb))
                             (val (gethash key memo)))
                        (cond (val (incf nhit) val)
                              (t (setq val (if (zerop bb)
                                               (ack (1- aa) 1)
                                               (ack (1- aa) (ack aa (1- bb)))))
                                 (setf (gethash key memo) val)
                                 val)))))))
      (let ((ret (ack m n)))
        (format t "A(~d,~d)=~:d (~:d calls, ~:d cache hits)~%"
                m n ret ncal nhit)
        (values ret memo)))))

如您所见,我对小的 a 使用显式公式,对较大的 a 使用记忆化。

然而,请注意,这个函数增长得如此之快,以至于试图计算实际值毫无意义;你会更快地耗尽宇宙中的原子——无论是否记忆。

【讨论】:

    【解决方案2】:

    从概念上讲,堆栈溢出与速度无关,但它们与 space 的使用有关。例如,考虑以下length 的实现。第一个将遇到长列表的堆栈溢出。第二个也会,除非你的 Lisp 实现了尾调用优化。第三个不会。不过,它们都具有相同的时间复杂度(速度);它们在列表的长度上是线性的。

    (defun length1 (list)
      (if (endp list)
          0
          (+ 1 (length1 (rest list)))))
    
    (defun length2 (list)
      (labels ((l2 (list len)
                 (if (endp list)
                     len
                     (l2 (rest list) (1+ len)))))
        (l2 list 0)))
    
    (defun length3 (list)
      (do ((list list (rest list))
           (len 0 (1+ len)))
          ((endp list) len)))
    

    您可以对您的代码执行类似的操作,但您仍然会有一个会占用堆栈空间的递归调用。因为这看起来确实是Ackermann function,所以我将使用zerop 而不是zpack 而不是foo。因此,您可以这样做:

    (defun foo2 (x y)
      (do () ((zp x) (+ 1 y))
        (if (zp y)
            (setf x (1- x)
                  y 1)
            (psetf x (1- x)
                   y (foo x (1- y))))))
    

    由于x 在每次迭代中都会减少1,并且唯一的条件更改是y,因此您可以将其简化为:

    (defun ack2 (x y)
      (do () ((zerop x) (1+ y))
        (if (zerop y)
            (setf x (1- x)
                  y 1)
            (psetf x (1- x)
                   y (ack2 x (1- y))))))
    

    由于 y 是唯一在迭代期间有条件地改变的东西,您可以进一步简化为:

    (defun ack3 (x y)
      (do ((x x (1- x))
           (y y (if (zerop y) 1 (ack3 x (1- y)))))
          ((zerop x) (1+ y))))
    

    这是一个昂贵的计算函数,它会让你走得更远,但你仍然无法到达,例如,(ackN 3 1000000)。所有这些定义都可以从http://pastebin.com/mNA9TNTm 轻松复制和粘贴。

    【讨论】:

    • 尾部优化版本可以不用do循环写吗?
    • Common Lisp 不保证尾调用优化(但 Scheme 可以)。如果你想确保你得到迭代,你必须使用迭代构造。您可以使用loop 而不是do,但您必须编写某种迭代。
    【解决方案3】:

    通常,memoization 是您在此类计算中的朋友。可能不适用,因为它取决于递归中的特定参数;但这是一种有用的探索方法。

    【讨论】:

    • +1 - 但是请注意,对于这个特定的功能,没有任何技巧可以解决问题 :-) 因为它增长太快了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-15
    • 2020-07-05
    • 2016-02-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多