Идиоматический Clojure для решения алгоритма динамического программирования

Я решил проработать текст CLRS Introduction to Algorithms и аккуратно выбрал проблему печати herher.

Я проработал проблему и нашел императивное решение, которое было просто реализовать на Python, но несколько сложнее в Clojure.

Я совершенно запутался в переводе функции вычислений-матрицы из моего решения в идиоматический Clojure. Какие-либо предложения? Вот псевдокод для функции calculate-matrix:

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

EDIT: я обновил свою функцию построения матрицы с учетом отзывов, так что теперь она фактически на одну строку короче, чем моя реализация 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)))))

person GrooveStomp    schedule 06.11.2010    source источник


Ответы (4)


  1. Вместо того, чтобы использовать let с fn, попробуйте letfn.
  2. дозыq дозыq -> похоже, что это, вероятно, было бы лучше для понимания
  3. Ваш конд/ноль? / = 1 код будет легче читать (и быстрее) с регистром.
  4. Мое паучье чутье подсказывает мне, что цикл/повторение здесь должен быть каким-то вызовом карты.
  5. Я сильно подозреваю, что это было бы намного быстрее с примитивными массивами (и, возможно, в некоторых местах чище)
  6. Вы можете использовать или посмотреть источник для Incanter.
person Alex Miller    schedule 06.11.2010
comment
1. Отличное предложение. Я понятия не имел, что существует такая вещь, как letfn. 2. Я изо всех сил пытался понять, как построить матрицу с использованием дозы. Надеюсь, есть больше предложений для практических примеров? :) 4. Я использовал некоторые карты, но в итоге использовал '_' для игнорирования некоторых значений и считал это нечистым, из-за отсутствия лучшего слова. Однако это может быть более идиоматично, как я недавно видел в некоторых примерах. 5. Что вы подразумеваете под «примитивными массивами»? Спасибо за ответ! (ответ сокращен, чтобы соответствовать ограничениям комментариев) - person GrooveStomp; 07.11.2010

Ваши функции матричного поиска и набора матриц могут быть упрощены. Вы можете использовать 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))

Алекс Миллер упомянул об использовании примитивных массивов. Если вам в конечном итоге нужно двигаться в этом направлении, вы можете начать с просмотра int-array, aset-int и aget. Дополнительные сведения см. в документации clojure.core. .

person Jake McCrary    schedule 08.11.2010
comment
Мне это нравится. get-in на самом деле проще, чем мой поиск по матрице, поэтому я просто убрал эту функцию и сократил кодовую базу. - person GrooveStomp; 08.11.2010
comment
Я также рассмотрю примитивные массивы. Спасибо за информацию! - person GrooveStomp; 08.11.2010

Я взобрался на стену и смог мыслить достаточно похоже на Clojure, чтобы преобразовать базовый алгоритм вычислительной матрицы в работоспособную программу.

Это всего на одну строку длиннее, чем моя реализация Python, хотя она выглядит более плотно написанной. Конечно, такие понятия, как «карта» и «уменьшение», являются функциями более высокого уровня, которые требуют, чтобы вы надели свои мыслительные способности.

Я считаю, что эта реализация также исправляет ошибку в моем 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]))))

Спасибо Алекс и Джейк за ваши комментарии. Они оба были очень полезны и помогли мне на моем пути к идиоматичному Clojure.

person GrooveStomp    schedule 09.11.2010

Мой общий подход к динамическим программам в 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

Я считаю, что мемоизация легче обернуть мой мозг, чем мутирующие таблицы.

person Ara Vartanian    schedule 07.07.2014