繁体   English   中英

优化在Haskell中实现的BFS

[英]Optimizing BFS implemented in 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函数将生成算法采取的步骤。

我担心的是状态会被复制到每一步。 我意识到与++的连接效果不佳,但是老实说,这无关紧要,因为所有状态都在每一步都被复制。

我知道有些单子应该做我在这里要做的事情,但是由于Haskell是纯净的,这是否意味着单子仍然必须复制状态?

难道没有办法说“嘿,我在代码中只使用了这些值一次,并且没有将它们存储在任何地方。您可以更改它们,而不必创建新的值”?

如果Haskell自己做到这一点,它仍然可以使我保持代码的纯净,但可以使执行速度更快。

您的状态仅在修改后才被复制-不会在使用时被复制。

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

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

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

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

有两种方法可以减少queue复制,而有两种方法可以减少nodes复制。

对于nodes您可以将计算包装在ST s单子中,以使用单个可修改向量。 或者,您可以使用像IntMap这样的功能性数据结构,它具有相当快的更新速度

对于您的queue您可以使用Data.Sequence或两个列表的实现

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

同样,从Prelude iterate ,可以更简洁地编写bfsStep

现在,除了queue上的O(n)之外, bfs所有内容都是O(1) 但是, (++)在其第一个参数的长度上仅为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)

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

如果您真的想使用命令式数据结构来跟踪您去过的地方,我建议您使用ST monad。 不幸的是,让ST使用我上面提到的队列类型有点麻烦。 我不确定我会推荐这种组合,尽管从FP的心态来看,这种组合并没有什么错。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM