2017-12-12 3 views
1

Wie würde ich das Äquivalent des folgenden Pythons in clojure codieren, aber strikt die Rekursion für die Stapelverwaltung verwenden (zB keine Schleife verwenden/rekurrieren mit einem Vektor als Grenze)? Mir ist klar, dass es ziemlich einfach ist, wenn ein Vektor deine Pfade pflegt und nur späht/knallt, aber ich tue dies als Gedankenübung.Wie implementiert man ein rekursives DFS in Clojure (ohne Verwendung eines Vektors/Stacks)

Python Version


def dfs(start,successors,goal,visited=set()): 
    if start not in visited: 
     visited.add(start) 
     for s in successors.get(start): 
      if goal(s): 
       return s 
      else: 
       res = dfs(s,successors) 
       if res: return res #bail early when found 
    return False 

Clojure Version


(defn dfs [start goal? successors visited] 
    (if (goal? start) 
     start 
     (when (not (contains? visited start)) 
      (mapcat #(dfs % goal? successors (conj visited start)) 
         (successors start))))) 

Da Iteration durch einen Aufruf gesteuert wird, in der Clojure Version abzubilden, können Sie nicht wirklich früh auf die Art, wie du c ein in Python z.B. if goal(s): return s. Da Sie die rekursiven Aufrufe innerhalb einer Liste mit Map sammeln, müssen Sie jeden möglichen Knoten auch nach dem Fund des Ziels überprüfen. Erst dann, wenn alle Knoten erkundet sind, erhalten Sie das Ergebnis.


Jetzt weiß ich, dass ich so etwas tun kann (ich weiß, dass das nicht schön ist ... versuche nur ein kurzes Beispiel zu geben, zögere nicht, Verbesserungen vorzuschlagen!), Aber ich bin hauptsächlich daran interessiert zu sehen, ob es da ist eine Möglichkeit, die Verwendung eines Stacks explizit zu vermeiden und den Call-Stack wie in der Python-Version arbeiten zu lassen.

(defn dfs-non-rec [frontier goal? successors visited] 
    (loop [f frontier g? goal? s successors v visited] 
     (let [node (peek f)] 
      (cond ; case 1 
        (goal? node) 
        node 
        ;case 2 
        (not (contains? v node)) 
        (recur (vec (concat (pop f) (successors node))) g? s (conj v node)) 
        ;case 3 
        :else 
        (recur (pop f) g? s (conj v node)))))) 

Wie soll ich das angehen?

EDIT


Es gab einige Verwirrung meinerseits, ob einige der Antworten waren in der Tat zuerst in die Tiefe. Die Verwirrung rührte von einer Annahme her, die ich über die Eingabe gemacht habe, die ich ursprünglich in diesem Beitrag hätte geben sollen. Ich hatte den Eingang als Adjazenzliste angenommen, das eine Graph, anstatt ein Baum

(def graph {"a" ["b","c","d"], 
      "b" ["a","e","f"], 
      "c" ["x","y"], 
      "d" [], 
      "e" [], 
      "f" [], 
      "x" ["c"], 
      "y" ["e"]}) 

dann dar, wenn auf eine seq umgewandelt, gefolgt ist die Reihenfolge in-fact depth-first für dass Der sich ergebende Baum wird durch Aufrufen von seq auf dem Graph erstellt, jedoch wird die Reihenfolge, die durch die Adjazenzliste impliziert ist, nicht befolgt, da die Graphstruktur in der Konvertierung verloren geht.

Also, wenn Sie für den Knoten x ab dem a suchen, würde ich die Traversierfolgeliste erwarten adcyex zu sein, anstatt abcdbaefcxy

Antwort

2

zunächst alles, was Sie nicht wirklich brauchen Baum für Schleifen zu überprüfen, da die Datenstrukturen clojure habe keine Zirkelverweise (außer du verwendest keinen veränderlichen Zustand mit atom s, der auf andere Atome verweist, was ein offensichtlicher Code-Geruch ist). Der einfache Weg von Traversal könnte wie folgt aussehen (auf diese Weise wird von vielen Lisp verwiesen (und die allgemeinen Programmierung) Bücher):

user> (defn dfs [goal? data] 
     (if (goal? data) 
      data 
      (loop [data data] 
      (when-let [[x & xs] (seq data)] 
       (cond (goal? x) x 
        (coll? x) (recur (concat x xs)) 
        :else (recur xs)))))) 

user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]]) 
10 

user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]]) 
nil 

zusätzlich gibt prägnanter sind (und damit idiomatische i guess) Möglichkeiten, dies zu tun in clojure. Die einfachste ist tree-seq zu verwenden:

user> (defn dfs [goal? tree] 
     (first (filter goal? (tree-seq coll? seq tree)))) 
#'user/dfs 

user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]]) 
10 

user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]]) 
nil 

user> (dfs (every-pred number? even?) [1 [3 5 [7 9] [10] 11 12]]) 
10 

tree-seq faul ist, so dass es durchläuft nur Baum, bis Sie den gewünschten Wert finden.

eine andere Art und Weise ist clojure zippers zu gebrauchen:

user> (require '[clojure.zip :as z]) 
nil 

user> (defn dfs [goal? tree] 
     (loop [curr (z/zipper coll? seq identity tree)] 
      (cond (z/end? curr) nil 
       (goal? (z/node curr)) (z/node curr) 
       :else (recur (z/next curr))))) 
#'user/dfs 

user> (dfs #{10} [1 [3 5 [7 9] [10] 11 12]]) 
10 

user> (dfs #{100} [1 [3 5 [7 9] [10] 11 12]]) 
nil 

user> (dfs (every-pred number? even?) [1 [3 5 [7 9] [10] 11 12]]) 
10 
+0

Ich bin mir nicht sicher, ob ich über die Prüfung für Zyklen Teil zustimmen. Selbst mit unveränderlichen Strukturen können Sie immer noch Grafikzyklen haben. wenn der Graph bidirektional ist, z. '{: a [: a: b]: b [: b: a]}' dann würden Sie in einer Endlosschleife stecken bleiben, ohne die besuchten Knoten im Auge zu behalten. – Solaxun

+0

@Solaxun richtig, für Grafiken, die Sie beschreiben, ist es so. Aber für einfache Bäume, wo alle Kinder die Elemente von verschachtelten Sammlungen sind, wie in diesem Fall, macht es einfach keinen Sinn. – leetwinski

+0

Ich hätte in meiner Beschreibung vielleicht expliziter sein können, nehme ich an, aber ich habe diesen Input nie angegeben oder gesagt Baum :) Auch - ist dein erstes Beispiel nicht wirklich ein BFS? Es sieht so aus, als ob du die Kinder 'xs' von links nach rechts verarbeitest. Wie würde das funktionieren, wenn Sie mit einer Diagrammstruktur arbeiten, in die Sie an einem bestimmten Punkt einsteigen, untergeordnete Elemente erstellen und von dort aus fortfahren möchten, anstatt den gesamten Baum sequenziell zu bearbeiten. – Solaxun

0

Ich würde es wie die folgenden tun, die the Tupelo library zum Testen verwendet:

(ns tst.demo.core 
    (:use tupelo.test) 
    (:require [tupelo.core :as t])) 

(def data [[1 2 [3]] 
      [[4 5] 6] 
      [7]]) 

(def search-result (atom nil)) 
(defn search-impl 
    [goal? data] 
    (when (= ::not-found @search-result) 
    (if (goal? data) 
     (reset! search-result data) 
     (when (coll? data) 
     (doseq [it data] 
      (search-impl goal? it)))))) 

(defn search [goal? data] 
    (reset! search-result ::not-found) 
    (search-impl goal? data) 
    @search-result) 

(dotest 
    (println "1 => " (search #(= 5 %) data)) 
    (println "2 => " (search #(and (integer? %) (even? %)) data)) 
    (println "3 => " (search #(= [4 5] %) data)) 
    (println "4 => " (search #(= 99 %) data))) 

mit den Ergebnissen:

1 => 5 
2 => 2 
3 => [4 5] 
4 => :tst.demo.core/not-found 

Haben Sie keine Angst, ein bisschen veränderlichen Zustand (in diesem Fall ein Atom) zu verwenden, wenn es Ihr Programm klarer und/oder einfacher macht.

Wenn Sie wirklich das Atom verstecken wollen von global sichtbar zu sein, tun gerade dies:

(defn search2-impl 
    [search2-result goal? data] 
    (when (= ::not-found @search2-result) 
    (if (goal? data) 
     (reset! search2-result data) 
     (when (coll? data) 
     (doseq [it data] 
      (search2-impl search2-result goal? it)))))) 

(defn search2 [goal? data] 
    (let [search2-result (atom ::not-found)] 
    (search2-impl search2-result goal? data) 
    @search2-result)) 

(dotest 
    (println "21 => " (search2 #(= 5 %) data)) 
    (println "22 => " (search2 #(and (integer? %) (even? %)) data)) 
    (println "23 => " (search2 #(= [4 5] %) data)) 
    (println "24 => " (search2 #(= 99 %) data))) 
21 => 5 
22 => 2 
23 => [4 5] 
24 => :tst.demo.core/not-found 
Verwandte Themen