【问题标题】:Breadth-First Search using State monad in Haskell在 Haskell 中使用 State monad 进行广度优先搜索
【发布时间】:2015-02-17 23:34:24
【问题描述】:

最近,我在 Stackoverflow 中问了一个关于从 Graph 构建 DFS 树的问题,并了解到它可以通过使用 State Monad 来简单地实现。

DFS in haskell

虽然 DFS 要求只跟踪访问过的节点,因此我们可以使用“Set”或“List”或某种线性数据结构来跟踪访问过的节点,但 BFS 需要“访问过的节点”和“队列”数据结构完成。

我的 BFS 伪代码是

Q = empty queue
T = empty Tree
mark all nodes except u as unvisited
while Q is nonempty do
u = deq(Q)
    for each vertex v ∈ Adj(u)
        if v is not visited 
        then add edge (u,v) to T
             Mark v as visited and enq(v)

从伪代码可以推断,我们每次迭代只需执行 3 个过程。

  1. 队列中的出队点
  2. 将该点的所有未访问邻居添加到当前树的子节点、队列和“已访问”列表中
  3. 对队列中的下一个重复此操作

由于我们没有使用递归遍历进行 BFS 搜索,我们需要一些其他的遍历方法,例如 while 循环。我在 hackage 中查找了 loop-while 包,但它似乎有些过时了。

我假设我需要这样的代码:

{-...-}
... =   evalState (bfs) ((Set.singleton start),[start])
where 
    neighbors x = Map.findWithDefault [] x adj 
    bfs =do (vis,x:queue)<-get
             map (\neighbor ->
                  if (Set.member neighbor vis)
                  then put(vis,queue) 
                  else put ((Set.insert neighbor vis), queue++[neighbor]) >> (addToTree neighbor)
                 )  neighbors x
            (vis,queue)<-get
         while (length queue > 0)

我知道这个实现是非常错误的,但这应该为我认为应该如何实现 BFS 提供简约的观点。另外,我真的不知道如何规避使用 while 循环 for do 块。(即我应该使用递归算法来克服它还是应该考虑完全不同的策略)

考虑到我在上面链接的上一个问题中找到的答案之一,答案似乎应该是这样的:

newtype Graph a = Graph (Map.Map a [a]) deriving (Ord, Eq, Show)
data Tree a = Tree a [Tree a] deriving (Ord, Eq, Show)

bfs :: (Ord a) => Graph a -> a -> Tree a
bfs (Graph adj) start = evalState (bfs') ((Set.singleton start),[start])
    where
        bfs' = {-part where I don't know-}

最后,如果由于某种原因无法使用 state monad 实现 BFS,(我认为不是)请纠正我的错误假设。

我在 Haskell 中看到了一些不使用 state monad 的 BFS 示例,但我想了解更多关于如何处理 state monad 的信息,但找不到任何使用 state monad 实现的 BFS 示例。

提前致谢。


编辑: 我想出了某种使用状态单子的算法,但我陷入了无限循环。

bfs :: (Ord a) => Graph a -> a -> Tree a
bfs (Graph adj) start = evalState (bfs' (Graph adj) start) (Set.singleton start)

bfs' :: (Ord a) => Graph a -> a -> State (Set.Set a) (Tree a)
bfs' (Graph adj) point= do
                        vis <- get
                        let neighbors x = Map.findWithDefault [] x adj
                        let addableNeighbors (x:xs) =   if Set.member x vis
                                                        then addableNeighbors(xs)
                                                        else x:addableNeighbors(xs)
                        let addVisited (vis) (ns) = Set.union (vis) $ Set.fromList ns
                        let newVisited = addVisited vis $ addableNeighbors $ neighbors point
                        put newVisited
                        return (Tree point $ map (flip evalState newVisited) (map (bfs' (Graph adj)) $ addableNeighbors $ neighbors point))

EDIT2:以空间复杂性为代价,我提出了一种使用图返回和排队处理来获取 BFS 图的解决方案。尽管它不是生成 BFS 树/图的最佳解决方案,但它会起作用。

bfs :: (Ord a) => Graph a -> a -> Graph a
bfs (Graph adj) start = evalState (bfs' (Graph adj) (Graph(Map.empty))  [start]) (Set.singleton start)


bfs':: (Ord a) => Graph a -> Graph a -> [a] -> State (Set.Set a) (Graph a)
bfs' _ (Graph ret) [] = return (Graph ret)
bfs' (Graph adj) (Graph ret) (p:points)= do
                                        vis <- get
                                        let neighbors x = Map.findWithDefault [] x adj
                                        let addableNeighbors ns
                                                | null ns = []
                                                | otherwise =   if Set.member (head ns) vis
                                                                then addableNeighbors(tail ns)
                                                                else (head ns):addableNeighbors(tail ns)
                                        let addVisited (v) (ns) = Set.union (v) $ Set.fromList ns
                                        let unVisited = addableNeighbors $ neighbors p
                                        let newVisited = addVisited vis unVisited
                                        let unionGraph (Graph g1) (Graph g2) = Graph (Map.union g1 g2)
                                        put newVisited
                                        bfs' (Graph adj) (unionGraph (Graph ret) (Graph (Map.singleton p unVisited))) (points ++ unVisited)

EDIT3:我添加了将图形转换为树的功能。在 EDIT2 和 EDIT3 中运行函数将产生 BFS 树。它不是计算时间方面最好的算法,但我相信它对于像我这样的新手来说是直观且易于理解的:)

graphToTree :: (Ord a) => Graph a -> a -> Tree a
graphToTree (Graph adj) point  = Tree point $ map (graphToTree (Graph adj)) $ neighbors point
    where neighbors x = Map.findWithDefault [] x adj

【问题讨论】:

  • 将命令式算法翻译成函数式语言充其量是困难的,有时甚至是不可能的。将伪代码“一对一”翻译成 Haskell 可能会非常难看。一个好的起点是函数bfs' :: Ord a =&gt; Graph a -&gt; a -&gt; State (S.Set a) (Tree a),它从给定节点开始执行BFS,S.Set a是访问节点。您不需要保留节点队列 - 在命令式设置中,这很方便,但这里不是这种情况。

标签: algorithm haskell state-monad breadth-first-search


【解决方案1】:

将图表转换为Tree 广度优先比简单的searching the graph breadth-first 更困难一些。如果您正在搜索图表,您只需要从一个分支返回。将图转成树时,结果需要包含多个分支的结果。

我们可以使用比Graph a 更通用的类型来搜索或转换为树。我们可以使用函数a -&gt; [a] 搜索或转换为树。对于Graph,我们将使用函数(Map.!) m,其中mMap。使用转置表搜索有一个类似的签名

breadthFirstSearchUnseen:: Ord r => (a -> r) -> -- how to compare `a`s 
                           (a -> Bool) ->       -- where to stop
                           (a -> [a]) ->        -- where you can go from an `a`
                           [a] ->               -- where to start
                           Maybe [a]

将函数转换为包含最早深度的每个可达节点的树有一个类似的签名

shortestPathTree :: Ord r => (a -> r) -> -- how to compare `a`s
                    (a -> l)             -- what label to put in the tree
                    (a -> [a]) ->        -- where you can go from an `a`
                    a ->                 -- where to start
                    Tree l

我们可以稍微更一般地从任意数量的节点开始,构建一个Forest,其中包含最早深度的每个可达节点。

shortestPathTrees :: Ord r => (a -> r) -> -- how to compare `a`s
                     (a -> l)             -- what label to put in the tree
                     (a -> [a]) ->        -- where you can go from an `a`
                     [a] ->               -- where to start
                     [Tree l]

搜索

执行到树的转换并不能真正帮助我们搜索,我们可以在原始图上执行广度优先搜索。

import Data.Sequence (viewl, ViewL (..), (><))
import qualified Data.Sequence as Seq
import qualified Data.Set as Set

breadthFirstSearchUnseen:: Ord r => (a -> r) -> (a -> Bool) -> (a -> [a]) -> [a] -> Maybe [a]
breadthFirstSearchUnseen repr p expand = combine Set.empty Seq.empty []
    where
        combine seen queued ancestors unseen =
            go
                (seen  `Set.union` (Set.fromList . map repr            $ unseen))
                (queued ><         (Seq.fromList . map ((,) ancestors) $ unseen))
        go seen queue =
            case viewl queue of
                EmptyL -> Nothing
                (ancestors, a) :< queued ->
                    if p a
                    then Just . reverse $ ancestors'
                    else combine seen queued ancestors' unseen
                    where
                        ancestors' = a:ancestors
                        unseen = filter (flip Set.notMember seen . repr) . expand $ a

在上述搜索算法中维护的状态是一个Seq 队列,其中包含接下来要访问的节点和一个Set 已经见过的节点。如果我们改为跟踪已经访问过的节点,那么如果我们在相同深度找到到节点的多条路径,我们就可以多次访问同一个节点。在我写这个广度优先搜索的答案中有一个more complete explanation

我们可以很容易地按照我们的一般搜索写搜索Graphs。

import qualified Data.Map as Map

newtype Graph a = Graph (Map.Map a [a]) deriving (Ord, Eq, Show)

bfsGraph :: (Ord a) => Graph a -> (a -> Bool) -> [a] -> Maybe [a]
bfsGraph (Graph adj) test = breadthFirstSearchUnseen id test ((Map.!) adj)

我们也可以自己写如何搜索Trees。

import Data.Tree

bfsTrees :: (Ord a) => (a -> Bool) -> [Tree a] -> Maybe [a]
bfsTrees test = fmap (map rootLabel) . breadthFirstSearchUnseen rootLabel (test . rootLabel) subForest

建树

构建树的广度优先是a lot more difficult。幸运的是Data.Tree 已经提供了从一元展开中以广度优先顺序构建Trees 的方法。广度优先顺序将负责排队,我们只需要跟踪我们已经看到的节点的状态。

unfoldTreeM_BF 的类型为 Monad m =&gt; (b -&gt; m (a, [b])) -&gt; b -&gt; m (Tree a)m 是我们的计算将在其中的Monadb 是我们将基于此构建树的数据类型,a 是树标签的类型。为了使用它来构建一棵树,我们需要创建一个函数b -&gt; m (a, [b])。我们将把a 重命名为l 作为标签,将b 重命名为a,这是我们一直用于节点的名称。我们需要创建一个a -&gt; m (l, [a])。对于m,我们将使用来自transformersState monad 来跟踪某些状态;状态将是我们已经看到其表示r 的节点的Set;我们将使用State (Set.Set r) monad。总的来说,我们需要提供一个函数a -&gt; State (Set.Set r) (l, [a])

expandUnseen :: Ord r => (a -> r) -> (a -> l) -> (a -> [a]) -> a -> State (Set.Set r) (l, [a])
expandUnseen repr label expand a = do
    seen <- get
    let unseen = filter (flip Set.notMember seen . repr) . uniqueBy repr . expand $ a
    put . Set.union seen . Set.fromList . map repr $ unseen
    return (label a, unseen)

为了构建树,我们运行unfoldForestM_BF构建的状态计算

shortestPathTrees :: Ord r => (a -> r) -> (a -> l) -> (a -> [a]) -> [a] -> [Tree l]
shortestPathTrees repr label expand = run . unfoldForestM_BF k . uniqueBy repr
    where
        run = flip evalState Set.empty
        k = expandUnseen repr label expand

uniqueBy 是一个nubBy,它利用Ord 实例而不是Eq

uniqueBy :: Ord r => (a -> r) -> [a] -> [a]
uniqueBy repr = go Set.empty
    where
        go seen []     = []
        go seen (x:xs) =
            if Set.member (repr x) seen
            then go seen xs
            else x:go (Set.insert (repr x) seen) xs

我们可以根据我们一般的最短路径树构建来编写从Graphs 构建最短路径树

shortestPathsGraph :: Ord a => Graph a -> [a] -> [Tree a]
shortestPathsGraph (Graph adj) = shortestPathTrees id id ((Map.!) adj)

我们可以用同样的方法将Forest 过滤为仅通过Forest 的最短路径。

shortestPathsTree :: Ord a => [Tree a] -> [Tree a]
shortestPathsTree = shortestPathTrees rootLabel rootLabel subForest

【讨论】:

    【解决方案2】:

    我的解决方案基于逐级工作(对 BFS 而言),另请参阅 this question and answer

    一般的想法是:假设我们已经知道在我们的 BFS 的每个级别之前访问元素的集合作为集合列表。然后我们可以逐级遍历图,更新我们的集合列表,在途中构造输出Tree

    诀窍在于,在这样的逐级遍历之后,我们将在每一级之后获得访问元素的集合。这与每个级别 before 的列表相同,只是移动了一个。所以通过tying the knot,我们可以使用移位后的输出作为过程的输入。

    import Control.Monad.State
    import qualified Data.Map as M
    import Data.Maybe (fromMaybe, catMaybes)
    import qualified Data.Set as S
    import Data.Tree
    
    newtype Graph a = Graph (M.Map a [a])
        deriving (Ord, Eq, Show)
    
    tagBfs :: (Ord a) => Graph a -> a -> Maybe (Tree a)
    tagBfs (Graph g) s = let (t, sets) = runState (thread s) (S.empty : sets)
                          in t
      where
        thread x = do
            sets@(s : subsets) <- get
            case M.lookup x g of
                Just vs | not (S.member x s) -> do
                    -- recursively create sub-nodes and update the subsets list
                    let (nodes, subsets') = runState
                                              (catMaybes `liftM` mapM thread vs) subsets
                    -- put the new combined list of sets
                    put (S.insert x s : subsets')
                    -- .. and return the node
                    return . Just $ Node x nodes
                _ -> return Nothing -- node not in the graph, or already visited
    

    在下面的例子中运行tagBfs example2 'b'

    example2 :: Graph Char
    example2 = Graph $ M.fromList
        [ ('a', ['b', 'c', 'd'])
        , ('b', ['a'])
        , ('c', [])
        , ('d', [])
        ]
    

    产量

    Just (Node {rootLabel = 'b',
                subForest = [Node {rootLabel = 'a',
                                   subForest = [Node {rootLabel = 'c',
                                                      subForest = []},
                                                Node {rootLabel = 'd',
                                                      subForest = []}
                                               ]}
                            ]}
          )
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-01-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-25
      • 2016-02-16
      相关资源
      最近更新 更多