【问题标题】:How can I generate series of Pell numbers instead of a specific one in Lisp如何在 Lisp 中生成一系列 Pell 数字而不是特定数字
【发布时间】:2019-08-08 23:00:47
【问题描述】:

如何使用 cons 或其他方式打印Pell numbers 的列表直到第 N 个数字?

(defun pellse (k)
   (if (or (zerop k) (= k 1))
       k
   (+ (* 2 (pellse (- k 1)))
      (pellse (- k 2)))))
 (print (pellse 7))

【问题讨论】:

  • 你好@ziming-wang,你已经写了计算第N个佩尔数的Lisp代码,请问如何将它们累积到一个列表中,例如(pellse(1), pellse(2), ..., pellse(N))
  • 没错,例如。像这样的列表: (0 1 2 5 12 29 70) 其中 n 的值为 6。

标签: lisp common-lisp


【解决方案1】:

以下是如何以非指数级的方式做到这一点:

(defun pells (n)
  (loop repeat n
    for current = 0 then next
    and next = 1 then (+ (* 2 next) current)
    collect current))

给定前两个元素,计算第 n 个元素的时间复杂度为 O(log(Pn)) 其中 Pn 是第 n 个 Pell 数;您需要 log(Pn) 位作为答案和 log(Pn) 操作进行加法。我们实际上不需要计算 Pn 是什么:它由简单的线性递推关系定义,因此解必须是指数的,所以 log(P n) = O(n)。因此计算前 n 个 Pell 数的复杂度为 O(n*n) = O(n2 )。

不能[*]比O(n2)做得更好,因为必须写O( n2) 位来表示所有这些数字。

[*] 虽然我非常怀疑这一点,但从理论上讲,通过某种方式共享数据以更紧凑的方式表示列表是可能的。

【讨论】:

    【解决方案2】:

    这是解决此问题的一种方法,它通过定义无限的 Pell 数流来工作。这是基于SICP,尤其是section 3.5 中提出的想法。每个人都应该读这本书。

    首先,我们需要定义一个结构,让我们讨论无限数据结构。我们通过延迟评估除了其中有限部分之外的所有内容来做到这一点。因此,从一个名为 delay 的宏开始,它会延迟对表单的评估,返回一个“承诺”(当然这是一个函数),以及一个名为 force 的函数,它会强制系统兑现其承诺:

    (defmacro delay (form)
      ;; Delay FORM, which may evaluate to multiple values.  This has
      ;; state so the delayed thing is only called once.
      (let ((evaluatedp-n (make-symbol "EVALUATEDP"))
            (values-n (make-symbol "VALUES")))
        `(let ((,evaluatedp-n nil) ,values-n)
           (lambda ()
             (unless ,evaluatedp-n
               (setf ,evaluatedp-n t
                     ,values-n (multiple-value-list
                                (funcall (lambda () ,form)))))
             (values-list ,values-n)))))
    
    (defun force (promise)
      ;; force a promise (delayed thing)
      (funcall promise))
    

    (对于我们的目的来说,这个实现有点过于复杂,但这是我必须要做的。)。

    现在我们将使用delay 来定义,它们可能是无限的conses 链。这些上的操作对应于 conses 上的操作,但前缀为stream-,还有一个名为null-stream 的对象对应于()(实际上在这个实现中是同一个对象)。

    (defmacro stream-cons (car cdr)
      ;; a cons whose cdr is delayed
      `(cons ,car (delay ,cdr)))
    
    (defun stream-car (scons)
      ;; car of a delayed cons
      (car scons))
    
    (defun stream-cdr (scons)
      ;; cdr of a delayed cons, forced
      (force (cdr scons)))
    
    (defconstant null-stream
      ;; the empty delayed cons
      nil)
    
    (defun stream-null (stream)
      ;; is a delayed cons empty
      (eq stream null-stream))
    

    现在定义一个函数pell-stream,它返回一个佩尔数流。此函数手工制作流的前两个元素,然后使用生成器制作其余元素。

    (defun pell-stream ()
      ;; A stream of Pell numbers
      (labels ((pell (pn pn-1)
                 (let ((p (+ (* 2 pn) pn-1)))
                   (stream-cons p (pell p pn)))))
        (stream-cons 0 (stream-cons 1 (pell 1 0)))))
    

    现在我们可以简单地重复使用stream-cdr 来计算佩尔数。

    (defun n-pell-numbers (n)
      (loop repeat n
            for scons = (pell-stream) then (stream-cdr scons)
            collect (stream-car scons)))
    

    现在

    > (n-pell-numbers 20)
    (0
     1
     2
     5
     12
     29
     70
     169
     408
     985
     2378
     5741
     13860
     33461
     80782
     195025
     470832
     1136689
     2744210
     6625109)
    

    请注意,事实上,pell-stream 可以是全局变量:它不需要是函数:

    (defparameter *pell-stream*
      (labels ((pell (pn pn-1)
                 (let ((p (+ (* 2 pn) pn-1)))
                   (stream-cons p (pell p pn)))))
        (stream-cons 0 (stream-cons 1 (pell 1 0)))))
    
    (defun n-stream-elements (stream n)
      (loop repeat n
            for scons = stream then (stream-cdr scons)
            collect (stream-car scons)))
    

    如果我们定义一个小基准测试程序:

    (defun bench-pell (n)
      (progn (n-stream-elements *pell-stream* n) n))
    

    然后有趣的是,这显然本质上是一个线性过程(对于后面的元素它会变慢,因为数字变大,因此对它们的操作需要很长时间),并且承诺的有状态实现使其第一次迭代后快得多(以保留相当多的 bignums 为代价):

    > (time (bench-pell 100000))
    Timing the evaluation of (bench-pell 100000)
    
    User time    =        2.020
    System time  =        0.803
    Elapsed time =        2.822
    Allocation   = 1623803280 bytes
    441714 Page faults
    100000
    
    > (time (bench-pell 100000))
    Timing the evaluation of (bench-pell 100000)
    
    User time    =        0.007
    System time  =        0.000
    Elapsed time =        0.006
    Allocation   = 1708248 bytes
    0 Page faults
    100000
    

    【讨论】:

    • 为什么选择定义流而不是使用系列?
    • @DanRobertson:因为对我来说,“这是使用基本语言结构构建这个有趣的解决方案的方法”比“这是一个可以解决问题的庞大、不透明、优化的工具”更有趣。当然,我使用了loop,因此有点破坏了我自己的论点。
    • @DanRobertson:话虽如此,我欢迎使用系列的答案!
    • @WillNess 所以这里定义的流就像一个 Haskell 列表(除了汽车总是被强制的),因为计算在完成时存储在内存中。 cltl2 中定义的series 表示如何计算一系列值但不存储它们。系列的典型用途是消耗它产生的值。如果想保留值,请将它们放入一个数组中以便更快地查找。
    • @DanRobertson 对。但这不是全部,因为除了被自动记忆(正如您所指出的那样没什么大不了的)之外,它还可以自动可扩展,即在/从任意点暂停和重新启动顺序。不知道系列设施中是否有这种能力?粗略地看一下 CLtL2 章节,似乎不是。如果是这样,this 可能是您所询问的 原因 的优势。
    【解决方案3】:

    一种可能的解决方案是使用 Common Lisp 的 LOOP 宏,例如:

    (print
        (loop for x in '(1 2 3 4 5 6 7)
          for y = (pellse x)
          collect y))
    

    打印出以下结果:

    (1 2 5 12 29 70 169)
    

    基于此,可以构建如下函数:

    (defun list-of-n-pell-numbers (n)
        (loop for x from 0 to n
              for y = (pellse x)
              collect y))
    

    然后像下面这样运行它:

    (print (list-of-n-pell-numbers 7))
    (0 1 2 5 12 29 70 169)
    

    但是在使用这段代码时请小心,因为你对pellse函数的定义是递归的,并且有堆栈溢出的风险:让它重复调用自己足够多(例如对于N的大值),它可能炸毁调用堆栈,除非你做一些尾递归。您可能需要检查以下说明:

    【讨论】:

    • (list-of-n-pell-numbers 7) 返回前 8 个,而不是 7 个 Pell 数字。您可以改用(loop :for x :below n :collect (pellse x))
    • 如果 pellse 是线性的,这将是低效的二次方,但由于它已经是指数的,我想这并不重要。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-07-02
    • 2010-09-16
    • 2016-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多