避免广度优先搜索中的重复

时间:2014-07-12 13:37:36

标签: algorithm haskell functional-programming

出于教育目的,我最近在Haskell中实现了常用算法。目前我被困在广度优先搜索上。这是我的实现,为简单起见,节点只表示为整数:

import qualified Data.Map as M
import qualified Data.List as L

type Node = Int
type Graph = M.Map Node [Node]

-- Returns list of nodes adjacent to n in graph g
adjacent :: Node -> Graph -> [Node]
adjacent n g = M.findWithDefault [] n g

-- Returns graph g with all instances of n removed
rip :: Node -> Graph -> Graph
rip n g = M.delete n (M.map (L.delete n) g)

bfs :: Node -> Graph -> [Node]
bfs n g = [n] ++ _bfs [n] g

_bfs :: [Node] -> Graph -> [Node]
_bfs (n:ns) g =
    if not (M.null g) then
        let layer = adjacent n g in
            layer ++ _bfs (ns ++ layer) (rip n g)
    else n:ns
_bfs [] g = []

(实际构建图形还有其他功能,但为了简洁起见,我将它们遗漏了)

调用bfs的结果将是图的正确广度优先遍历,如果不是因为某些图形产生重复的事实,例如这个:

The graph

bfs 1 g g的结果=此图表为[1,2,3,4,4,5,6,7,7,7]

我目前的解决方案归结为将_bfs中的相关行更改为L.nub $ layer ++ _bfs (ns ++ layer) (rip n g),但这看起来非常黑,而且我不确定它是否会产生正确的广度优先遍历。除了在插入之前不断检查n:ns重复项(听起来非常低效),我没有其他想法。

如何重写_bfs(或更多),以便根据定义不会产生重复?

2 个答案:

答案 0 :(得分:4)

您应该使用一组访问过的节点而不是rip

首先,rip占用剩余边数的线性时间,这使得整个广度优先遍历为二次方。

其次,对于rip,无重复遍历是不切实际的。目前,添加了重复节点,因为可以从当前遍历边界的多个节点访问相同的节点。重新访问不能简单地用rip修剪,因为它完全从图中删除了节点,但我们仍然需要节点才能继续遍历。

以下是State monad中访问集的示例(这里很不错,因为我们可以通过边界构建遍历边界,而来自filterM的{​​{1}}非常方便好吧,过滤掉访问过的节点):

Control.Monad

以下是没有import qualified Data.IntMap.Strict as IM import qualified Data.IntSet as IS import Control.Monad import Control.Monad.State.Strict type Node = Int type Graph = IM.IntMap [Node] bfs :: Node -> Graph -> [Node] bfs n g = evalState (go [n]) (IS.singleton n) where go :: [Node] -> State IS.IntSet [Node] go [] = return [] go ns = do ns' <- flip filterM ((g IM.!) =<< ns) $ \n' -> do notVisited <- gets (IS.notMember n') when notVisited $ modify (IS.insert n') return notVisited (ns++) `fmap` go ns' -- your example graph graph :: Graph graph = IM.fromList $ [ (1, [2, 3]) , (2, [1, 4]) , (3, [1, 4]) , (4, [2, 5, 3, 6]) , (5, [4, 7]) , (6, [4, 7]) , (7, [5, 6])] main = print $ bfs 1 graph -- [1, 2, 3, 4, 5, 6, 7] 的相同算法的实现,而是使用State传递更新的访问集:

foldr

答案 1 :(得分:1)

您需要记住到目前为止已遍历的节点并传递该信息。并使用具有高效成员资格测试操作的数据结构。一个好的候选人是Set,或者如果你只使用Int节点,那么IntSet