【问题标题】:How to implement a recursive DFS in Clojure (without using a vector / stack)如何在 Clojure 中实现递归 DFS(不使用向量/堆栈)
【发布时间】:2017-12-12 23:43:19
【问题描述】:

如何在 clojure 中编写等效于以下 python 的代码,但严格使用递归进行堆栈管理(例如,不使用循环/递归,向量作为边界)?
我意识到用一个矢量来保持你的路径并且只是偷看/弹出是相当简单的,但我这样做是作为一个思考练习。

Python 版本


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 版本


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

由于在 Clojure 版本中迭代是由对 map 的调用控制的,因此您不能像在 Python 中那样提前放弃,例如if goal(s): return s
由于您正在使用 map 收集列表内的递归调用,因此即使在找到目标之后,您也必须评估每个可能的节点。只有在探索完所有节点之后,您才能获得结果。


现在,我知道我可以做这样的事情(我知道这并不漂亮......只是想提供一个简单的例子,随时提出改进建议!)但我主要感兴趣的是看看是否有办法避免显式使用堆栈,并让调用堆栈像在 python 版本中那样工作。
(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)))))) 

我应该如何处理这个问题?

编辑


对于所提供的某些答案是否实际上是深度优先的,我有些困惑。混乱源于我对输入的假设,我最初应该在这篇文章中提供。我假设输入是一个邻接列表,它代表一个 graph,而不是一个 tree
(def graph  {"a"  ["b","c","d"],
             "b"  ["a","e","f"],
             "c"  ["x","y"],
             "d"  [],
             "e"  [],
             "f"  [],
             "x"  ["c"],
             "y"  ["e"]})

然后,当转换为 seq 时,遵循的顺序实际上是深度优先 对于通过在图上调用 seq 创建的结果树,但是不遵循邻接列表隐含的顺序,因为图结构在转换中丢失了。

因此,如果您正在寻找从a 开始的节点x,我希望遍历顺序是adcyex,而不是abcdbaefcxy

【问题讨论】:

    标签: python clojure depth-first-search


    【解决方案1】:

    首先你并不需要检查循环树,因为 clojure 的数据结构没有循环引用(除非你不使用可变状态和 atoms 引用另一个原子,这是一个明显的代码气味)。简单的遍历方式可能如下所示(许多 lisp(和整体编程)书籍都引用了这种方式):

    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
    

    此外,在 clojure 中还有更简洁(因此我猜是惯用的)方法来做到这一点。最简单的就是使用tree-seq

    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 是惰性的,所以它只会遍历树,直到找到所需的值。

    另一种方法是使用clojure的zippers

    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
    

    【讨论】:

    • 我不确定我是否同意检查周期部分。即使使用不可变结构,您仍然可以拥有图形循环。如果图形是双向的,例如{:a [:a :b] :b [:b :a]} 那么您将陷入无限循环而无法跟踪访问过的节点。
    • @Solaxun 对,你描述的图表就是这样。但是对于所有子节点都是嵌套集合元素的简单树,就像在这种情况下,它只是没有意义
    • 我想我本可以更明确地描述我的描述,但我从未提供过输入或说它是一棵树 :) 另外 - 你的第一个例子不是真的在做 BFS 吗?看起来您从左到右处理孩子xs。如果您正在处理要在特定点输入、生成子节点并从那里继续的图形结构,而不是按顺序处理整个树,这将如何工作。
    • @Solaxun 这显然是一个 DFS,再看一遍算法,并添加跟踪(例如在 when-let 之后跟踪 x)。如果您可以在问题中添加一些所需的输入,那也很好,那么更容易推断解决方案的有效性))
    • 输入足够公平——我原本认为无论输入如何,DFS 都不会有差异,但我错了。我认为我的困惑是由于我假设邻接列表作为输入(请参阅更新的问题),它会在您遍历树时生成孩子。如果您随手生成,我认为提供的答案不能保证 DFS 遍历顺序,如果我错了,请纠正我。
    【解决方案2】:

    我会这样做,它使用the Tupelo library 进行测试:

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

    结果:

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

    当它使您的程序更清晰和/或更简单时,不要害怕使用一些可变状态(在本例中为原子)。

    如果你真的想隐藏原子不让全局可见,只需这样做:

    (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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-02
      • 1970-01-01
      • 2015-12-17
      • 2020-08-27
      • 2014-03-14
      • 2020-07-21
      • 1970-01-01
      相关资源
      最近更新 更多