Avoiding duplicates in breadth-first search

algorithm, functional-programming, haskell

Solution

You should use a set of visited nodes instead of `rip`.

First, `rip` takes linear time in the number of remaining edges, which makes the whole breadth-first traversal quadratic.

Second, the no-duplicates traversal is not practical with `rip`. Currently, duplicate nodes are added because the same nodes can be visited from multiple nodes of the current frontier of traversal. The revisits can't be simply pruned with `rip` because it removes the node from the graph altogether, but we still need the node in order to continue the traversal.

Here's an example with a visited set in a `State` monad (which is nice here, since we can build up the traversal frontier by frontier, and `filterM` from `Control.Monad` is handy for, well, filtering out visited nodes):

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]

Here's is an implementation of the same algorithm without `State`, instead using `foldr` to pass along the updated visited set:

bfs' :: Node -> Graph -> [Node]
bfs' start graph = go [start] (IS.singleton start) where

    go :: [Node] -> IS.IntSet -> [Node]
    go [] _       = []
    go ns visited = ns ++ go ns' visited' where

        newNodes = [n' | n <- ns, n' <- graph IM.! n]

        step n (acc, visited) 
            | IS.member n visited = (acc, visited)
            | otherwise = (n:acc, IS.insert n visited) 

        (ns', visited') = foldr step ([], visited) newNodes 

Problem

For educational purposes I've recently been implementing common algorithms in Haskell. Currently I'm stuck on Breadth-First Search. This is my implementation, with nodes being represented as just integers for simplicity: ``` 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 = [] ``` (There are other functions for actually constructing the graph, but I left them out for brevity's sake) The result of calling `bfs` would be a correct breadth-first traversal of the graph, if not for the fact that some graphs produce duplicates, such as this one: (The result of `bfs 1 g` for `g` = this graph is `[1,2,3,4,4,5,6,7,7,7]`) My current solution boils down to changing the relevant line in `_bfs` to `L.nub $ layer ++ _bfs (ns ++ layer) (rip n g)`, but that seems incredibly hackish, and I'm not sure if it'll produce a correct breadth-first traversal. Aside from constantly checking `n:ns` for duplicates before inserting (which sounds horribly inefficient), I have no other ideas. How can I rewrite `_bfs` (or more) so that it does not produce duplicates by definition?

Original source