0

I have no problem in implementing this algorithm in any imperative language, but I am struggling implementing it in Clojure or any other functional language. A lot of algorithms are described in terms of working with mutable data structures and imperative loops and it is hard for me to translate all of those to a functional domain.

Here is my incomplete attempt (a draft, not a working implementation) at implementing it in Clojure using adjacency lists as graph representation:

(ns karger.core
  (:require [clojure.string :as string]))

(defn load-data []
    (zipmap 
     (range 1 1000)
     (map rest (map read-string
       (string/split (slurp "data.txt") #"\n")))))

(defn min-cut [graph] 
  (let [start (rand-int (count graph))
        end (rand-int (graph start))
        start-list (nth graph start)]
    (for [x (graph end)
          :when (not= x start)]
      (assoc graph start (conj start-list x)))
      ))

(count (load-data))

Can anyone give me a reference implementation of this algorithm (preferably written in Clojure)? Also I would like if someone gave me a general advice of translating an algorithm described in imperative terms to a functional domain.

Thanks in advance!

UPDATE #1

Here is a link to algorithm implementation written in Python: http://pastebin.com/WwWCtxpu

Kirill Dubovikov
  • 1,457
  • 2
  • 21
  • 41
  • can you provide algorithm in imperative language? – edbond May 29 '14 at 12:23
  • Can you show what a line from data.txt looks like? It is hard to tell what is intended vs. what is an error if one doesn't know how the data from that file would shaped. I assume by the use of `read-string` and `rest` that each line is a Clojure sequence. – noisesmith May 29 '14 at 12:36
  • The abstract description doesn't look terribly imperative. http://en.wikipedia.org/wiki/Karger's_algorithm#Contraction_algorithm – Don Stewart May 29 '14 at 14:03
  • 1
    In general the idea in functional programming is to provide all "state" that would parameterize a function as additional arguments, and to return all "changes of state" in the function's return value (and then use these new values in the calling function). I started working on a pure functional clojure implementation of the algorithm, based on the solution [here](https://gist.github.com/MastaP/2314166) and passing the graph as the input and output to each function, but that is a bit much for a SO answer right now - I may follow up another time with a translation if no-one else does. – noisesmith May 29 '14 at 15:45
  • 1
    Further, the code here is very far from being an implementation of the algorithm, and is riddled with misuses of both datatypes and functions. – noisesmith May 29 '14 at 15:48
  • noisesmith, yes, it is only a draft. Thank you a lot, I am looking forward to see your code. The data.txt is a graph representation where each line is an adjacency list. – Kirill Dubovikov May 29 '14 at 15:56

1 Answers1

1

There are fundamental problems with your code as is:

  • your start-list binding is a number and cannot be conjed to'
  • you are calling assoc and ignoring the return value, thus making it a no-op.
  • you are using for as if it were a looping construct (it is a list comprehension)
  • you are calling nth on a hash-map, which will always fail (zipmap returns a hash-map)

In general the idea in functional programming is to lift mutable variables into immutable local bindings by making the "state of the world" entirely encapsulated by the function arguments, and making function calls with refined versions of that state.

Here is a working implementation from scratch based on the python solution you posted, and the graph file used by the java example here

(ns min-cut.core
  (:require [clojure.java.io :as io]
            [clojure.string :as string]
            [clojure.pprint :refer [pprint]]))

(defn make-maps
  [filename]
  (reduce (fn [graph line]
            (let [[node & edges] (->> line
                                      (#(string/split % #"\W+"))
                                      (remove #{""})
                                      (map read-string))]
              (assoc graph node (set edges))))
          {}
          (line-seq (io/reader filename))))

(defn karger
  [graph]
  (if (<= (count (keys graph))
          2)
    (count (graph (apply min (keys graph))))
    (let [start (rand-nth (keys graph))
          finish (rand-nth (vec (graph start)))
          graph (loop [g graph
                       [edge & edges] (seq (graph finish))]
                  (if-not edge
                    g
                    (recur
                     (if (= edge start)
                       g
                       (update-in g [start] conj edge))
                     edges)))
          graph (loop [g graph
                       [edge & edges] (seq (graph finish))]
                  (if-not edge
                    g
                    (let [gr (update-in g [edge] disj finish)
                          gr (if (= edge start)
                               gr
                               (update-in gr [edge] conj start))]
                      (recur gr edges))))
          graph (dissoc graph finish)]
      (recur graph))))

(defn -main
  [& [file]]
  (let [file (or file "kargerAdj.txt")
        graph (make-maps file)]
    (println "min cut is: "
             (reduce min (repeatedly 1801 #(karger graph))))))

This is a very literal translation of the python code, so there are multiple places this code could be improved. For starters the two loops in the karger function could likely be replaced by a single reduce which would be much more concise and clear.

Note that no value that is created in this code is ever mutated - values are rebound but none of the incoming data structures are changed, and the only global definitions used are the functions make-maps, karger, and -main - all data is locally bound and passed to the next user.

noisesmith
  • 20,076
  • 2
  • 41
  • 49
  • This could easily be made parallel with `core.reducers` so that each CPU is calculating attempts and the smallest are kept. – noisesmith May 29 '14 at 20:27