【问题标题】:Can bottom-up dynamic programming be done in Lisp?可以在 Lisp 中完成自下而上的动态编程吗?
【发布时间】:2011-12-11 02:35:34
【问题描述】:

典型的 Lisp 方言可以使用bottom-up "dynamic programming" 方法解决问题吗?

(请注意:我不是在谈论 “memoization”,据我所知,使用任何 Lisp 方言都是微不足道的。我真正谈论的是自下而上的动态编程,例如,您在其中构建数组,然后使用您刚刚介绍的元素来计算下一个元素。)

例如,使用动态规划,“0-1 背包”问题可以在伪多项式时间内解决任何其他方法都会失败的输入。

命令式(不完整)解决方案是:

for (int k = 1; k <= a.length; k++) {
    for (int y = 1; y <= b; y++) { 
        if (y < a[k-1]) {
            knap[k][y-1] = knap[k-1][y-1];
        } else {
            if (y > a[k-1]) {
                knap[k][y-1] = Math.max(knap[k-1][y-1], knap[k-1][y-1-a[k-1]] + c[k-1]);
            } else {
                knap[k][y-1] = Math.max(knap[k-1][y-1], c[k-1]);
    }
}

在各种 Lisp 方言中可以做到这一点吗?如果没有,为什么不呢?

【问题讨论】:

  • 我不确定我是否理解这个问题; Lisps 中算法的规范实现几乎总是递归和“自下而上”的。
  • @Dave Newton:我可能不太清楚......我指的是“自下而上”,而不是维基百科关于动态编程的文章中的“自上而下”,其中“记忆化” (保存幂等方法/函数调用的结果以供以后重用)将是“自上而下”,而从底部开始工作将是“自下而上”。这两种方法都可以将一些问题减少到伪多项式时间。但在这种情况下我对 memoization 不感兴趣:我想知道我是否可以从“自下而上”构建,比如数组(我计算单元格 1,然后我使用单元格 1计算单元格 2 等)
  • 是什么让你觉得在 Lisp 中不能做到这一点?
  • 你认为 Lisp 是一种函数式语言吗?尽管您可以以那种风格使用它(尤其是 Scheme,由于尾递归优化),并且您可以用它的语法做很多强大和/或奇怪的事情,但它仍然只是一种具有可变状态的命令式语言,就像 C . 你可以将你的 C 代码基本上“逐字”翻译成 Lisp。
  • @CedricMartin:Common Lisp 是一种多范式语言,使用它的特性来发挥全部作用并没有错。几乎所有现实世界的 Common Lisp 代码都使用可变对象、数组甚至全局变量。 AFAIK Scheme 程序员尽可能地避免使用命令式代码。

标签: algorithm clojure lisp dynamic-programming knapsack-problem


【解决方案1】:

这当然是可能的。您唯一需要的是数组、整数和循环结构。例如,在 Scheme 中,您的算法可以使用vectors 转录。主要问题是它变得难以阅读,因为knap[k-1][y-1] 变成了(vector-ref (vector-ref knap (- k 1)) (- y 1)) 并且

knap[k][y-1] = knap[k-1][y-1];

变成

(vector-set! (vector-ref knap k) (- y 1)
             (vector-ref (vector-ref knap (- k 1)) (- y 1)))

(或模数的一个棘手的技巧),而记忆递归只是保持可读性。

根据经验,我建议您在使用 Lisp 和类似语言进行编程时坚持记忆。如果使用哈希表,那么对于 memoization 和 DP,预期的渐近时间和空间复杂度是相同的。

【讨论】:

  • +1 这很有趣。所以你是说我应该坚持使用记忆,因为它不会使用更多的内存,而且更容易写/读?
  • @CedricMartin:是的。 Peter Norvig 早在 1990 年就证明,正确记忆的递归下降解析器等同于 CKY 算法(基于 DP 的解析器),类似地,许多 DP 问题可以表述为记忆递归。
  • +1。不过,附带评论,因为 OP 询问“各种 Lisp 方言”:Common Lisp 内置了多维数组,因此“难以阅读”部分,但与 C 中的特殊索引语法支持相比仍然正确, 在那里应用得不太强烈。例如,knap[k][y-1] = knap[k-1][y-1]; 变为 (setf (aref knap k (1- y)) (aref knap (1- k) (1- y)))
  • (另外,如果你真的做了很多数组处理,你总是可以写一个阅读器宏。:))
【解决方案2】:

请原谅我这么说,但是您引用的维基百科页面(imnsho)写得不是很好。特别是,它或多或少地制造了自顶向下和自底向上动态编程之间的二分法,并继续将其中一种描述为“更有趣”。两者之间的唯一区别是构建表的顺序。根据调用的顺序,记忆会产生两者

提前向撰写此页面部分的人道歉;感谢您的努力,我只是认为该部分需要一些工作。

【讨论】:

  • "memoization" 被明确定义为在后续调用中重用幂等方法调用的结果,以避免需要多次计算它们。从头开始构建数组时,从 cell1/line1 到 cell2/line1 到 cell3/line1 等,然后从 cell1/line2(重复使用 line1 中的单元格)到 cell2/line2 等是不是 i> 记忆。因为这里没有保存/重用幂等方法调用。 larsmans 的好答案似乎正是我所追求的,并且似乎与 “memoization” 的“我的”(和维基百科的那个)一致。
  • 我在 WP 文章中找不到“有趣”这个词。我只是快速扫描了它,它对术语 top-downbottom-up 的使用似乎与它们在解析中的使用相匹配,至少在 NLP 中所做的那样(选择的解析策略非常重要)。
  • @John Clements:现在我同意可以弥补的自上而下/自下而上。但我仍然认为我不会将他们所谓的“自下而上”方法称为 “memoization”(因为再一次,没有方法调用被重用,除此之外,通常“自下而上”的方法会计算出许多不一定会使用的结果......而在“记忆化”的方法中,只会计算实际将要使用的结果)。
  • @larsmans - 因为我在过去几天确实阅读了那篇文章以刷新自己,所以他们在这里使用它:“自下而上的方法:这是更有趣的案例。” - 来自本节:en.wikipedia.org/wiki/…
  • @John Clements:尖尖没问题; )这里stackoverflow.com/questions/6184869用户aaoibe(我写这篇文章时65K代表)解释了“自上而下”和“自下而上”之间的区别。据我回忆,自下而上的方法比自上而下的方法效果更好(更有效/更节省空间)存在具体问题。我不会说它们是相同的。也许“概念上”或“数学上”它们是相同的,但考虑到我们正在使用的设备的物理限制,我倾向于认为它们并不是一回事。
【解决方案3】:

简短的回答是肯定的,Clojure 可以直接使用 java 数组所以直接翻译 很拘谨

 (for [k (range 1 (count a)) 
       y (range 1 b)]
   (if (< y (aget a (dec k)))
     (aset knap k (dec y) (aget knap (dec k) (dec y))
           (if (> y (aget a (dec k)))
             (aset knap k (dec y) (max (aget knap (dec k) (dec y))
                                       (aget knap (dec k) (+ (- y 1 (aget a (dec k)))
                                                             (aget c (dec k))))
                                       (aset knap k (dec y) (max (aget knap (dec k) (dec y))
                                                                 (aget c (dec k))))))))))

这不是非常惯用的 Clojure,因为它将循环与要完成的工作结合在一起。 如果您将此循环的元素分开,生成的代码将更清晰,更容易显示正确。

作为微不足道的第一步,如果我们将循环与“工作”分开,那么我们就得到了。

(defn edit-array [k y]
   (if (< y (aget a (dec k)))
     (aset knap k (dec y) (aget knap (dec k) (dec y))
           (if (> y (aget a (dec k)))
             (aset knap k (dec y) (max (aget knap (dec k) (dec y))
                                       (aget knap (dec k) (+ (- y 1 (aget a (dec k)))
                                                             (aget c (dec k))))
                                       (aset knap k (dec y) (max (aget knap (dec k) (dec y))
                                                             (aget c (dec k))))))))))
(for [k (range 1 (count a)) y (range 1 b)]
   (edit-array k y))

然后我们可以从 repl 中测试编辑数组并说服自己它可以工作(也许还可以编写一个单元测试)。在那之后,也许您想开始更仔细地查看edit-array 并决定是否可以将其分解为更易于独立测试的步骤。也许您可以将其更改为使用功能样式而不是改变数组。在这里,我将远离您的具体问题,因为我不得不承认我不理解这个线性规划解决方案旨在解决的原始问题。

 (defn finished? [row] ... determine if we have reached the final state ...)

 (defn next-row [current-row]
    (for [y (range 1 (count current-row)]
      ... code to produce a new vector based on the previous one ...))

(take-while #(not (finished? %) (iterate next-row (initial-state)))

我看到的 Idomatic Clojure 代码的基本概念是将问题分解为简单的(只做一件事)抽象,然后将它们组合起来以解决主要问题。当然,这必须始终量身定制以适应手头的问题。

【讨论】:

  • 这不是很典型的 Clojure,因为它结合了循环和要完成的工作——你能扩展一下吗?
  • 谢谢,我正在尝试使用 Clojure,但从未以任何正式的方式学习过 FP。我一直听说命令式循环很糟糕,没有任何进一步的理由。我尽可能尝试使用内置迭代器,但有时会失败,例如,当迭代涉及多个集合时。
  • 一个小问题:在上面的最后一行 [(take-while...],current-row 的价值在哪里?
  • 无处,你泄露了我肮脏的小秘密!我发布了这个没有测试!羞耻耻辱
【解决方案4】:

这是 Clojure 中一个很好的自下而上的斐波那契版本(我相信最初是由 Christophe Grand 编写的):

(defn fib []
  (map first (iterate
              (fn [[a b]] [b (+ a b)])
              [0 1])))

这会生成一个无限的惰性序列,因此您可以要求尽可能多或尽可能少:

(take 10 (fib))
=> (0 1 1 2 3 5 8 13 21 34)

(nth (fib) 1000)
=> 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875

【讨论】:

    猜你喜欢
    • 2011-03-20
    • 2012-10-16
    • 2013-03-20
    • 1970-01-01
    • 1970-01-01
    • 2010-10-14
    • 2011-03-19
    • 1970-01-01
    相关资源
    最近更新 更多