优化在Haskell中实现的BFS

时间:2014-03-28 18:18:09

标签: algorithm haskell

所以我想写一个图广度优先搜索。该算法跟踪其状态中的某些值。它们是:每个节点和队列的visited状态。 它还需要知道图形的边缘以及它的目标目标是什么,但这不会逐步改变。

这就是我提出的(抱歉丑陋)

import Prelude hiding (take, cons, drop)
import Data.Vector

type BFSState = (Vector Bool, Vector [Int], [Int], Int)
bfsStep :: BFSState -> BFSState
bfsStep (nodes, edges, current:queue, target)
    | current == target = (nodes, edges, [], target)
    | nodes ! current = (nodes, edges, queue, target)
    | otherwise = (markedVisited, edges, queue Prelude.++ (edges ! current), target)
    where
        markedVisited = (take current nodes) Data.Vector.++ (cons True (drop (current + 1) nodes))

bfsSteps :: BFSState -> [BFSState]
bfsSteps init = steps
    where steps = init : Prelude.map bfsStep steps 

bfsStep获取状态并生成下一个状态。当状态队列为[]时,找到目标节点。 bfsSteps只使用自引荐列表制作BFSState列表。现在,目前没有办法找出到达某个节点需要多少步骤(给定起始条件),但bfsSteps函数将生成算法所采取的步骤。

我关心的是状态每一步都被复制。我意识到与++的连接并不能很好地运行,但我觉得它真的没关系,因为所有状态都会被复制到每一步。

我知道有些monad几乎应该做我在这里做的事情,但是既然Haskell是纯粹的,那不就意味着monad仍然需要复制状态吗?

不应该有办法说“嘿,我在我的代码中只使用这些值一次而且我没有将它们存储在任何地方。你可以改变它们而不是制作新的”?

如果Haskell自己做了这件事,它仍然允许我保持代码纯净,但快速执行。

3 个答案:

答案 0 :(得分:2)

您的状态仅在修改时复制 - 而不是在使用状态时复制。

例如,edges :: Vector [Int]永远不会被bfsStep修改,因此在所有递归调用中都会重复使用相同的值。

另一方面,bfsStep会以两种方式修改您的queue :: [Int]

  • 将其拆分为current : queue - 但这会重用原始队列的尾部,因此不会进行复制
  • 使用Prelude.++附加到它。这需要O(queue size)复制。

更新nodes :: Vector Int以包含新节点时,您需要进行类似的复制。

您可以通过几种方式减少对queue的复制,并通过几种方式减少对nodes的复制。

对于nodes,您可以将计算包装在ST s monad中以使用单个可修改的向量。或者,您可以使用功能数据结构,例如具有fairly fast updateIntMap

对于queue,您可以使用Data.Sequence或a two list implementation

答案 1 :(得分:2)

由于Edgestarget永远不会改变,我重写bfsStep只返回新的Nodesqueue。我还使用Data.Vector.modifyNodes进行了就地更新,而不是之前使用的笨拙take/drop/cons方法。

此外,bfsStep可以iterate更简洁地写为Prelude

现在,bfs中的所有内容均为O(1),但O(n)附加了queue个附加内容。但是,(++)在其第一个参数的长度上仅为O(n),因此如果每个顶点的边数较小,则效率非常高。

import Data.Vector (Vector)                                      
import qualified Data.Vector         as V
import qualified Data.Vector.Mutable as M                    

type Nodes = Vector Bool            
type Edges = Vector [Int]

bfs :: Nodes -> Edges -> [Int] -> Int -> (Nodes, [Int])
bfs nodes edges (x:xs) target              
    | x == target = (nodes, [])         
    | nodes V.! x = (nodes, xs)         
    | otherwise   = (marked, edges V.! x ++ xs)
    where marked = V.modify (\v -> M.write v x True) nodes 

bfsSteps :: Nodes -> Edges -> [Int] -> Int -> [(Nodes, [Int])]
bfsSteps nodes edges queue target = 
    iterate (\(n, q) -> bfs n edges q target) (nodes, queue)

答案 2 :(得分:2)

您可能有兴趣阅读我的Monad Reader文章的第一部分或第二部分:Lloyd Allison’s Corecursive Queues: Why Continuations Matter,它使用自引用来实现有效的队列。还有一些关于hackage的代码control-monad-queue。事实上,当我实现一个合理有效的广度优先图可达性算法时,我首先发现了这个技巧,尽管我使用了功能数据结构来跟踪算法已经看到的内容。

如果您真的想坚持使用命令式数据结构来跟踪您所处的位置,我建议使用ST monad。不幸的是让ST使用我上面提到的队列类型有点hacky;我不确定我是否可以推荐这种组合,虽然从FP心态来看,这种组合并没有太大的错误。

使用更加强制性的方法,您可能最好使用传统的两个堆栈队列,或者如果您真的想要一些额外的性能,则实现基于命令式数组块的命令性队列。