Как обрабатывать несколько переменных в реализации алгоритма Clojure? - PullRequest
0 голосов
/ 31 октября 2018

Я новичок в Clojure и пытаюсь учиться, внедряя в него некоторые алгоритмы. Алгоритм, который я пишу, предназначен для вычисления метрики узла betweenness centrality для структуры данных графа.

Функция в алгоритме (алгоритм Брандеса), которую я пытаюсь реализовать, выглядит следующим образом:

enter image description here

Здесь V - вершина графа, а s - начальный узел, из которого мы пытаемся вычислить и вернуть метрики кратчайшего пути S, Pred and sigma

Это то, что мне удалось придумать, используя loom для создания начального графика g для каждого начального узла start:

 (defn ss-shortest-path     
   [g start]   
    (let [nodeset (disj (nodes g) start)
        pred (apply assoc {} (interleave (nodes g) (repeat nil)))
        dist (apply assoc {start 0} (interleave nodeset (repeat -1)))
        sigma (apply assoc {start 1} (interleave nodeset (repeat 0)))
        stack []]
    (loop [queue (conj clojure.lang.PersistentQueue/EMPTY start)]
      (if (empty? queue)
        {:sigma sigma
         :pred pred
         :stack stack}
        (let [v (peek queue)
              stack (conj stack v)]
          (doseq [w (successors g v)]
            (when (= (dist w) -1)
              (do
                (conj queue w)
                (assoc dist w (+ 1 (dist v)))))
            (when (= (dist w) (+ 1 (dist v)))
                  (do
                    (assoc sigma w (+ (sigma w) (sigma v)))
                    (assoc pred w v))))
            (recur (pop queue)))))))

Я знаю, что структуры данных Clojure являются неизменяемыми, поэтому каждый раз, когда я вызываю conj или assoc в переменных pred, sigma, stack, dist, создается новая копия, и исходные переменные остаются такими, какие они есть.

Но я не хочу использовать изменяемые состояния, такие как atoms, refs, поскольку у меня есть ощущение, что я просто копирую императивный стиль, который я уже знаю.

Итак, я ищу помощи некоторых опытных клоюристов, чтобы помочь мне создать эту функцию в идиоматическом стиле.

Заранее спасибо.

Ответы [ 3 ]

0 голосов
/ 31 октября 2018

Я бы сделал две основные вещи: Сначала , алгоритм имеет состояние, состоящее из нескольких «переменных» (queue, stack и т. Д.). Сначала я бы построил функцию, которая представляет алгоритмическое состояние с использованием неизменяемой карты, например

(defn initialize-state [g start]
  (let [nodeset (disj (nodes g) start)]
    {:g g
     :nodeset nodeset
     :pred (apply assoc {} (interleave (nodes g) (repeat nil)))
     :dist (apply assoc {start 0} (interleave nodeset (repeat -1)))
     :sigma (apply assoc {start 1} (interleave nodeset (repeat 0)))
     :stack []
     :queue (conj clojure.lang.PersistentQueue/EMPTY start)
     :current-vertex nil}))

Затем я бы в REPL проверил, правильно ли инициализирована эта карта для различных вариантов g и start.

Второй , я бы разбил алгоритм на несколько небольших функций, которые принимают состояние в качестве ввода и возвращают состояние в качестве вывода, вот так ( код не сработает, необходимо заполнить недостающие части):

(defn next-vertex [state]
  {:pre [(state? state)]
   :post [(state? %)]}
  (let [v (peek (:queue state))]
    (-> state
        (update :stack conj v)
        (assoc :current-vertex v))))

(defn process-successor [state w]
  (let [dist-w (dist w)]
    (cond
      ;; fill in...
      )))

(defn process-successors [state]
  {:pre [(state? state)]
   :post [(state? %)]}
  (reduce
   process-successor
   state
   (successors (:g state) (:current-vertex state))))

(defn pop-queue [state]
  {:pre [(state? state)]
   :post [(state? %)]}
  (update state :queue pop))

Карты с ключами :pre и :post являются так называемыми предварительными и последующими условиями, и функция state? может быть реализована, например, как (defn state? [x] (and (map? x) (contains? x :queue))), просто как проверка работоспособности.

Обратите внимание, что для каждой написанной вами функции вы можете протестировать ее в REPL с некоторыми данными, чтобы убедиться, что она работает, прежде чем писать следующую функцию. Все эти функции теперь могут быть объединены в полный переход состояния с помощью comp:

(def next-state (comp pop-queue process-successors next-vertex))

Теперь окончательный алгоритм выглядит примерно так:

(defn ss-shortest-path [g start]
  (loop [state (initialize-state g start)]
    (if (empty? (:queue state))
      state
      (recur (next-state state)))))

Итак, подведем итог: реализовать алгоритм намного проще, если разбить его на более мелкие части , которые можно разработать и проверить индивидуально.

0 голосов
/ 31 октября 2018

Ни один из других ответов не говорит об этом в явной форме, поэтому я решил прояснить часть, касающуюся "неизменности".

Я бы сказал, что loop - правильная конструкция для использования здесь. Проблема с вашей настройкой - единственный аккумулятор, который есть в вашей петле - queue. Каждый бит данных, который изменяется от одной итерации к следующей, должен быть частью накопителей цикла.

В данном случае dist, sigma, pred и stack - это все данные, которые могут перейти от одной итерации цикла к другой, поэтому все они должны быть объявлены в квадратных скобках. петли. Затем, когда вам нужно обновить один из фрагментов данных, вы обновите то, что дано для recur:

(loop [queue (conj clojure.lang.PersistentQueue/EMPTY start)
       pred (apply assoc {} (interleave (nodes g) (repeat nil)))
       dist (apply assoc {start 0} (interleave nodeset (repeat -1)))
       sigma (apply assoc {start 1} (interleave nodeset (repeat 0)))
       stack []]

  (if (empty? queue)
    (Return everything)
    (recur (conj queue some-data)
           (assoc pred :some-key some-data)
           (assoc dist :another-key other-data)
           (assoc sigma :key data)
           (conj stack stack-data))))

Все, что дано recur (обновленные неизменяемые структуры в этом случае) будет тем, что доступно loop на следующей итерации.

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

0 голосов
/ 31 октября 2018

Справочная информация: вот Java-версия ALG: https://github.com/jgrapht/jgrapht/blob/master/jgrapht-core/src/main/java/org/jgrapht/alg/scoring/BetweennessCentrality.java


Прежде всего, вам нужно определить s, Pred, sigma. Вы также должны определить формат для g, v, start и т. Д.

Во-вторых, я не уверен, что это лучшее учебное упражнение. Вы можете заменить Java while, for и т. Д. На Clojure loop/recur, doseq и другие, но это все еще похоже на «принудительную посадку». Вероятно, вы могли бы учиться намного быстрее (и намного глубже!), Читая хорошие книги, такие как «Clojure for the Brave & True», «Getting Clojure» и т. Д.

Идея заключается в том, что небольшие, самостоятельные практические проблемы чрезвычайно эффективны для обучения, чем одна гигантская проблема.


И не забудьте в закладки:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...