【问题标题】:Is there way to represent static data in Haskell? Or is there any other elegant algorithm for DFS traversal in Haskell?有没有办法在 Haskell 中表示静态数据?或者在 Haskell 中还有其他优雅的 DFS 遍历算法吗?
【发布时间】:2017-02-25 04:14:01
【问题描述】:

我正在尝试使用递归算法构造 DFS 树。

这个伪代码是:

DFF(G)
Mark all nodes u as unvisited
while there is an unvisited node u do 
    DFS(u)

.

DFS(u)
Mark u as visited
for each v in u's neighbor do 
    if v is not marked
        DFS(v)

虽然我可以通过为未访问/访问过的节点构造某种数据结构,为它们分配动态分配或某种声明,以简单的方式用命令式语言轻松完成此任务,但对于 Haskell 来说,这是不可能的,因为 Haskell 的纯粹性阻止我在传递参数时更改数据。

data Graph a = Graph [(a,[a])] deriving (Ord, Eq, Show)
data Tree a = Node a [Tree a] deriving (Ord, Eq, Show)

type Point = (Int, Int)
type Edges = [Point]
type Path = [Point]

pathGraphFy :: Graph Point -> Point -> Tree (Point,Path)
pathGraphFy inputGraph point = getPathVertex inputGraph (point,[])

getPathVertex :: Graph Point -> (Point, Path) -> Tree (Point,Path)
getPathVertex inputGraph (point,path) = 
    Node (point,point:path) (map (getPathVertex inputGraph) [(x,(point:path))  | x<- neighbors, x `notElem` path])
    where neighbors = pointNeighbor inputGraph point

pointNeighbor :: Graph Point -> Point -> Edges
pointNeighbor (Graph (x:xs)) point = 
    if fst x == point then snd x else pointNeighbor (Graph(xs)) point

这是我使用 DFS-ish(或者更确切地说是 BFS-ish)算法进行图遍历的结果,但问题是它将再次访问不在点路径中的所有点。 (即如果存在循环,它将顺时针和逆时针两种方式遍历)

我也尝试过使用访问点对另一个 Graph 进行柯里化,但失败了,因为通过参数传递的 Graphs 仅在遍历中保存 Graph 的数据(即不是全局的)

如果只有动态分配或静态数据来保存全局级别的数据是可能的,这可以很容易地解决,但我对 Haskell 有点陌生,我无法在网上找到关于这个问题的答案。请帮助我:(提前谢谢。

(附注) 我尝试使用传递访问节点列表,但它不起作用,因为当递归返回时,访问节点列表也会返回,从而无法跟踪数据。如果有办法使“地图”或“列表”全局化,则可以通过这种方式实现。尽管下面的答案是仅链接的答案,但对不能(或不应该)实施的原因有很好的解释。

【问题讨论】:

    标签: algorithm haskell depth-first-search


    【解决方案1】:

    涉及传递和返回状态或使用状态单子的答案比这种方法更透明,但正如下面的论文所述,它效率不高,也不能很好地概括。也就是说,无论您在这个答案中需要什么,都值得学习状态单子并在 Haskell 中使用不可变数据。

    The paper linked in another 回答文件对所谓的归纳图的使用进行了相当学术的讨论。幸运的是,该论文的作者很友善地将这种方法实现为 Haskell 库,fgl。我将省略一些关于将数据附加到节点等的细节,并展示如何使用这个库实现 DFS。修改此算法以生成树而不是列表很容易,并且列表版本更加简洁。

    dfs :: Graph gr => [Node] -> gr a b -> [Node]
    dfs [] _ = []  
    -- this equation isn't strictly necessary, but it can improve performance for very dense graphs.
    dfs _ g | isEmpty g = [] 
    dfs (v:vs) g = case match v g of
        (Just ctx, g') -> v:dfs (suc' ctx ++ vs) g'
        _ -> dfs vs g
    

    这里的关键是match,它将一个图分解为所谓的一个顶点的Context 和剩余的图(匹配返回一个Maybe Context,以涵盖顶点不在图中的情况)。

    顶点Context 的概念是归纳图思想的核心:它被定义为一个元组

    (adjIn, nodeId, nodeLabel, adjOut)
    

    其中adjInadjOut(edgeLabel, nodeId) 对的列表。

    请注意,术语标签在这里使用松散,指的是附加到顶点或边的一般数据。

    suc' 函数接受一个上下文并返回一个节点列表,这些节点是上下文中节点的后继节点(adjOut,已删除边缘标签)。

    我们可以像这样构建一个图表

    这样的代码

    testGraph :: DynGraph g => gr a b
    testGraph =
        let nodes = [(i, "N" ++ show i) | i <- [1..5]]
            edges = [(2,1,"E21")
                    ,(4,1, "E41")
                    ,(1,3, "E13")
                    ,(3,4, "E34")
                    ,(3,5,"E35")
                    ,(5,2, "E52")]
            withNodes = insNodes nodes empty
            in insEdges edges withNodes
    

    调用dfs testGraph 产生[1,3,4,5,2]

    注意:我很无聊,偶然发现了这个问题,所以答案只是几个小时调查和实验的记录。

    【讨论】:

    • 感谢您的回答,我想选择所有答案作为我的问题的答案,但由于这是最直接和最普遍的答案,我将其作为答案。再次感谢。
    • 哇,这需要我认真弯腰才能绕住我的头。我仍然不明白他们如何对访问状态进行编码,必须再看一遍那篇论文。很好的例子,感谢您向我介绍这个概念!
    • @NiklasB。隐式图的真正聪明之处在于,访问状态根本不需要编码。函数match 产生(在Just 的情况下)v 上图的分解:示例中的图g' 是包含除v 之外的所有顶点的图, 以及除了包括v 的所有边。这是因为图表本质上是一个上下文列表,因此可以简单地删除给定的上下文(显然,以一种有效的方式对不可变数据执行此操作并非易事,但这就是库的用途:-))
    • 是的,同时我已经阅读了论文 :) 所以本质上,模式匹配允许您从图中删除最近“添加”的节点。当遍历图时,我们重新排列它,这样我们总是有一个归纳定义,其中当前节点就是那个添加的节点。聪明。
    【解决方案2】:

    没有什么可以阻止您在函数参数/返回值中编码状态。经典的 DFS 可能如下所示:

    import qualified Data.Map as Map
    import qualified Data.Set as Set
    
    newtype Graph a = Graph (Map.Map a [a]) deriving (Ord, Eq, Show)
    data Tree a = Tree a [Tree a] deriving (Ord, Eq, Show)
    
    dfs :: (Ord a) => Graph a -> a -> Tree a
    dfs (Graph adj) start = fst $ dfs' (Set.singleton start) start
      where
        neighbors x = Map.findWithDefault [] x adj
        dfs' vis x =
          let (subtrees, vis') =
                foldr
                  (\y (subtrees, vis) ->
                    if Set.member y vis
                      then (subtrees, vis)
                      else let vis' = Set.insert y vis
                               (t, vis'') = dfs' vis' y
                           in (t : subtrees, vis'')
                  )
                  ([], vis)
                  (neighbors x)
          in (Tree x subtrees, vis')
    

    除了Map/Set,您还可以使用persistent hash tablesinteger maps/sets,具体取决于您的节点类型。

    为避免显式状态,您应该使用state monad

    import Control.Applicative
    import Control.Monad.State
    import Control.Monad
    import Data.Maybe
    {- ... -}
    
    dfs :: (Ord a) => Graph a -> a -> Tree a
    dfs (Graph adj) start = evalState (dfs' start) (Set.singleton start)
      where
        neighbors x = Map.findWithDefault [] x adj
        dfs' x = Tree x . catMaybes <$>
          forM (neighbors x) (\y -> get >>= \vis ->
            if Set.member y vis
              then return Nothing
              else put (Set.insert y vis) >> Just <$> dfs' y)
    

    【讨论】:

    • 这似乎不对。访问过的节点应该在列表理解中的 dfs' 调用之间被记住。
    • @AndrásKovács 你说得对,如果没有状态单子,它比我想象的要复杂。
    • 我认为您将状态单子视为 FSM 并不完全正确。它只是一个上下文,您可以在其中操作正常的、不可变的值,而不是传递和返回它。绝对值得学习。使用正确的抽象(请参阅下面的答案),您可以以非常简洁的方式绕过状态传递/状态单子。就说明可用于一般实践的概念而言,这可能仍然是比我更好的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-19
    • 1970-01-01
    • 2011-11-04
    • 2017-05-22
    • 2011-01-09
    相关资源
    最近更新 更多