[英]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或两个列表的实现 。
由于Edges
和target
永远不会改变,因此我重写了bfsStep
以仅返回新的Nodes
和queue
。 我还使用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.