Lisp 可以实现自下而上的动态规划吗?

发布于 2024-12-10 13:20:27 字数 782 浏览 0 评论 0原文

典型的 Lisp 方言可以使用自下而上的“动态编程”方法解决问题吗?

(请注意:我不是在谈论“记忆化”,据我所知,使用任何 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 方言中是否可以做到这样的事情?如果不是,为什么不呢?

Can a typical Lisp dialect solve problems using the bottom-up "dynamic programming" approach?

(Please note: I'm not talking about "memoization" which, as far as I understand, is trivial using any Lisp dialect. I'm really talking about bottom-up Dynamic Programming, where you build, for an example, your array bottom up and then use the elements you just introduced to compute the next ones.)

For example, using dynamic programming, the "0-1 knapsack" problem can be solved in pseudo-polynomial time for inputs on which any other method would fail.

An imperative (incomplete) solution is:

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]);
    }
}

Is such a thing possible to do in the various Lisp dialects? If no, why not?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

月下客 2024-12-17 13:20:27

当然这是可能的。您唯一需要的是数组、整数和循环结构。例如,在Scheme中,您的算法可以使用 向量。主要问题是它变得难以阅读,因为 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 和类似语言编程时坚持记忆。如果使用哈希表,则记忆化和 DP 的预期渐近时间和空间复杂度是相同的。

Of course this is possible. The only things you need are arrays, integers and a loop construct. E.g., in Scheme, your algorithm can be transcribed using vectors. The main problem is that it becomes hard to read, since knap[k-1][y-1] becomes (vector-ref (vector-ref knap (- k 1)) (- y 1)) and

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

becomes

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

(or a hairy trick with moduli), while memoized recursions just remain readable.

Speaking from experience, I recommend you stick to the memoization when programming in Lisp and similar languages. If you use hash tables, the expected asymptotic time and space complexity are the same for memoization and DP.

青衫儰鉨ミ守葔 2024-12-17 13:20:27

简短的回答是肯定的,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 代码的基本概念是将问题分解为简单的(只做一件事)抽象,然后组合它们来解决主要问题。当然,这必须始终根据当前的问题进行定制。

the short answer is yes, Clojure can work directly with java arrays so a direct translation
is very straitforward

 (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))))))))))

this is not very idomatic Clojure because it combines the looping with the work to be done.
The resulting code will be much cleaner and easier to show correct if you seperate the elements of this loop out.

As a trivial first step, If we seperate the looping from the 'work' then we get.

(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))

Then we can test edit array from the repl and convince our selves that it works (and write a unit test perhaps). After that, perhaps you would like to start to look at edit-array more closely and decide if it is possible to break this into steps that are easier to test independently. Perhaps you could change this to use a functional style instead of mutating an array. Here I will move away from your specific problem because I have to admit that I dont understand the original problem this linear programming solution was designed to solve.

 (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)))

The basic notion of what I see Idomatic Clojure code looking like is to decompose the problem into simple (does only one thing) abstractions and then compose them to solve the main problem. Of course this must always be tailored to suit the problem at hand.

好听的两个字的网名 2024-12-17 13:20:27

请原谅我这样说,但是您引用的维基百科页面(imnsho)写得不是很好。特别是,它或多或少地制造了自上而下和自下而上的动态规划之间的二分法,并继续将其描述为“更有趣”。两者之间的唯一区别是构建表的顺序。记忆化会产生这两种情况,具体取决于调用的顺序。

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

Pardon me for saying this, but the wikipedia page you refer to is (imnsho) not very well-written. In particular, it more or less fabricates the dichotomy between top-down and bottom-up dynamic programming, and goes on to describe one as "more interesting". The only difference between the two is the order in which the table is constructed. Memoization gives rise to both of these, depending on the order in which the calls are made.

Apologies in advance to whoever wrote this section of the page; I appreciate your effort, I just think that the section needs some work.

小矜持 2024-12-17 13:20:27

这是 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

Here's a nice bottom-up version of Fibonacci in Clojure (originally written by Christophe Grand, I believe):

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

This generates an infinite lazy sequence, so you can ask for as much or as little as you like:

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

(nth (fib) 1000)
=> 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文