用于求解动态规划算法的惯用 Clojure
Posted
技术标签:
【中文标题】用于求解动态规划算法的惯用 Clojure【英文标题】:Idiomatic Clojure for solving dynamic programming algorithm 【发布时间】:2011-05-05 23:05:15 【问题描述】:我决定通读 CLRS Introduction to Algorithms 文本,并选择了打印整齐的问题here。
我解决了这个问题,并提出了一个命令式解决方案,该解决方案在 Python 中很容易实现,但在 Clojure 中则不那么简单。
我对将计算矩阵函数从我的解决方案转换为惯用的 Clojure 感到非常困惑。有什么建议?下面是计算矩阵函数的伪代码:
// n is the dimension of the square matrix.
// c is the matrix.
function compute-matrix(c, n):
// Traverse through the left-lower triangular matrix and calculate values.
for i=2 to n:
for j=i to n:
// This is our minimum value sentinal.
// If we encounter a value lower than this, then we store the new
// lowest value.
optimal-cost = INF
// Index in previous column representing the row we want to point to.
// Whenever we update 't' with a new lowest value, we need to change
// 'row' to point to the row we're getting that value from.
row = 0
// This iterates through each entry in the previous column.
// Note: we have a lower triangular matrix, meaning data only
// exists in the left-lower half.
// We are on column 'i', but because we're in a left-lower triangular
// matrix, data doesn't start until row (i-1).
//
// Similarly, we go to (j-1) because we can't choose a configuration
// where the previous column ended on a word who's index is larger
// than the word index this column starts on - the case which occurs
// when we go for k=(i-1) to greater than (j-1)
for k=(i-1) to (j-1):
// When 'j' is equal to 'n', we are at the last cell and we
// don't care how much whitespace we have. Just take the total
// from the previous cell.
// Note: if 'j' < 'n', then compute normally.
if (j < n):
z = cost(k + 1, j) + c[i-1, k]
else:
z = c[i-1, k]
if z < optimal-cost:
row = k
optimal-cost = z
c[i,j] = optimal-cost
c[i,j].row = row
此外,我非常感谢您对我的 Clojure 源代码的其余部分提供反馈,特别是关于它的惯用程度。到目前为止,我是否设法在命令式范式之外充分思考我编写的 Clojure 代码?这里是:
(ns print-neatly)
;-----------------------------------------------------------------------------
; High-order function which returns a function that computes the cost
; for i and j where i is the starting word index and j is the ending word
; index for the word list "word-list."
;
(defn make-cost [word-list max-length]
(fn [i j]
(let [total (reduce + (map #(count %1) (subvec word-list i j)))
result (- max-length (+ (- j i) total))]
(if (< result 0)
nil
(* result result result)))))
;-----------------------------------------------------------------------------
; initialization function for nxn matrix
;
(defn matrix-construct [n cost-func]
(let [; Prepend nil to our collection.
append-empty
(fn [v]
(cons nil v))
; Like append-empty; append cost-func for first column.
append-cost
(fn [v, index]
(cons (cost-func 0 index) v))
; Define an internal helper which calls append-empty N times to create
; a new vector consisting of N nil values.
; ie., [nil[0] nil[1] nil[2] ... nil[N]]
construct-empty-vec
(fn [n]
(loop [cnt n coll ()]
(if (neg? cnt)
(vec coll)
(recur (dec cnt) (append-empty coll)))))
; Construct the base level where each entry is the basic cost function
; calculated for the base level. (ie., starting and ending at the
; same word)
construct-base
(fn [n]
(loop [cnt n coll ()]
(if (neg? cnt)
(vec coll)
(recur (dec cnt) (append-cost coll cnt)))))]
; The main matrix-construct logic, which just creates a new Nx1 vector
; via construct-empty-vec, then prepends that to coll.
; We end up with a vector of N entries where each entry is a Nx1 vector.
(loop [cnt n coll ()]
(cond
(zero? cnt) (vec coll)
(= cnt 1) (recur (dec cnt) (cons (construct-base n) coll))
:else (recur (dec cnt) (cons (construct-empty-vec n) coll))))))
;-----------------------------------------------------------------------------
; Return the value at a given index in a matrix.
;
(defn matrix-lookup [matrix row col]
(nth (nth matrix row) col))
;-----------------------------------------------------------------------------
; Return a new matrix M with M[row,col] = value
; but otherwise M[i,j] = matrix[i,j]
;
(defn matrix-set [matrix row col value]
(let [my-row (nth matrix row)
my-cel (assoc my-row col value)]
(assoc matrix row my-cel)))
;-----------------------------------------------------------------------------
; Print the matrix out in a nicely formatted fashion.
;
(defn matrix-print [matrix]
(doseq [j (range (count matrix))]
(doseq [i (range (count matrix))]
(let [el (nth (nth matrix i) j)]
(print (format "%1$8.8s" el)))) ; 1st item max 8 and min 8 chars
(println)))
;-----------------------------------------------------------------------------
; Main
;-----------------------------------------------------------------------------
;-----------------------------------------------------------------------------
; Grab all arguments from the command line.
;
(let [line-length (Integer. (first *command-line-args*))
words (vec (rest *command-line-args*))
cost (make-cost words line-length)
matrix (matrix-construct (count words) cost)]
(matrix-print matrix))
编辑:我已经根据给出的反馈更新了我的矩阵构造函数,所以现在它实际上比我的 Python 实现短了一行。
;-----------------------------------------------------------------------------
; Initialization function for nxn matrix
;
(defn matrix-construct [n cost-func]
(letfn [; Build an n-length vector of nil
(construct-empty-vec [n]
(vec (repeat n nil)))
; Short-cut so we can use 'map' to apply the cost-func to each
; element in a range.
(my-cost [j]
(cost-func 0 j))
; Construct the base level where each entry is the basic cost function
; calculated for the base level. (ie., starting and ending at the
; same word)
(construct-base-vec [n]
(vec (map my-cost (range n))))]
; The main matrix-construct logic, which just creates a new Nx1 vector
; via construct-empty-vec, then prepends that to coll.
; We end up with a vector of N entries where each entry is a Nx1 vector.
(let [m (repeat (- n 1) (construct-empty-vec n))]
(vec (cons (construct-base-vec n) m)))))
【问题讨论】:
【参考方案1】:-
不要使用带 fn 的 let,而是尝试 letfn。
doseq doseq -> 看起来它可能会更好地作为理解工具
你的条件/零? / = 1 代码使用大小写会更容易阅读(也更快)。
我的蜘蛛侠直觉告诉我,这里的循环/重复应该是某种地图调用
我强烈怀疑使用原始数组会更快(并且在某些地方可能更干净)
您可能想使用或查看Incanter 的源代码
【讨论】:
1.很好的建议。我不知道有 letfn 这样的东西。 2. 我很难理解使用doseq 构造矩阵。希望有更多关于实际例子的建议? :) 4. 我使用了一些地图,但最后使用 '_' 来忽略一些值,并认为这是“不纯的”,因为没有更好的词。不过,正如我最近在一些示例中看到的那样,它可能更惯用。 5.“原始数组”是什么意思?感谢您的反馈! (缩短答案以适应评论限制)【参考方案2】:您的矩阵查找和矩阵集函数可以简化。您可以使用assoc-in
和get-in
来操作嵌套的关联结构。
(defn matrix-lookup [matrix row col]
(get-in matrix [row col]))
(defn matrix-set [matrix row col value]
(assoc-in matrix [row col] value))
Alex Miller 提到使用原始数组。如果您最终需要朝那个方向发展,您可以先查看int-array
、aset-int
和aget
。查看clojure.core 文档以了解更多信息。
【讨论】:
我喜欢这个。 get-in 实际上比我的矩阵查找更简单,所以我只是删除了那个函数并缩短了我的代码库。 我也会研究原始数组。感谢您的信息!【参考方案3】:我爬上了墙,能够以一种非常类似于 Clojure 的方式思考,将核心计算矩阵算法转化为可行的程序。
它只比我的 Python 实现长了一行,尽管它看起来写得更密集。当然,像“map”和“reduce”这样的概念是更高层次的功能,需要你去思考。
我相信这个实现也修复了我的 Python 中的一个错误。 :)
;-----------------------------------------------------------------------------
; Compute all table entries so we can compute the optimal cost path and
; reconstruct an optimal solution.
;
(defn compute-matrix [m cost]
(letfn [; Return a function that computes 'cost(k+1,j) + c[i-1,k]'
; OR just 'c[i-1,k]' if we're on the last row.
(make-min-func [matrix i j]
(if (< j (- (count matrix) 1))
(fn [k]
(+ (cost (+ k 1) j) (get-in matrix [(- i 1) k])))
(fn [k]
(get-in matrix [(- i 1) k]))))
; Find the minimum cost for the new cost: 'cost(k+1,j)'
; added to the previous entry's cost: 'c[i-1,k]'
(min-cost [matrix i j]
(let [this-cost (make-min-func matrix i j)
rang (range (- i 1) (- j 1))
cnt (if (= rang ()) (list (- i 1)) rang)]
(apply min (map this-cost cnt))))
; Takes a matrix and indices, returns an updated matrix.
(combine [matrix indices]
(let [i (first indices)
j (nth indices 1)
opt (min-cost matrix i j)]
(assoc-in matrix [i j] opt)))]
(reduce combine m
(for [i (range 1 (count m)) j (range i (count m))] [i j]))))
感谢 Alex 和 Jake 的 cmets。他们都非常有帮助,并且帮助我走向惯用的 Clojure。
【讨论】:
【参考方案4】:我在 Clojure 中处理动态程序的一般方法不是直接搞乱值矩阵的构造,而是将记忆与定点组合器结合使用。这是我计算编辑距离的示例:
(defn edit-distance-fp
"Computes the edit distance between two collections"
[fp coll1 coll2]
(cond
(and (empty? coll1) (empty? coll2)) 0
(empty? coll2) (count coll1)
(empty? coll1) (count coll2)
:else (let [x1 (first coll1)
xs (rest coll1)
y1 (first coll2)
ys (rest coll2)]
(min
(+ (fp fp xs ys) (if (= x1 y1) 0 1))
(inc (fp fp coll1 ys))
(inc (fp fp xs coll2))))))
这里与天真的递归解决方案的唯一区别是简单地将递归调用替换为对 fp 的调用。
然后我创建一个记忆固定点:
(defn memoize-recursive [f] (let [g (memoize f)] (partial g g)))
(defn mk-edit-distance [] (memoize-recursive edit-distance-fp))
然后调用它:
> (time ((mk-edit-distance)
"the quick brown fox jumped over the tawdry moon"
"quickly brown foxes moonjumped the tawdriness"))
"Elapsed time: 45.758 msecs"
23
我发现记忆比修改表格更容易让我的大脑思考。
【讨论】:
以上是关于用于求解动态规划算法的惯用 Clojure的主要内容,如果未能解决你的问题,请参考以下文章