packages feed

algraph-0.7.0.0: src/Data/Graph/AdjacencyList/BFS.hs

{-|
Module      : Data.Graph.AdjacencyList.BFS
Description : Breadth-first search on adjacency-list graphs
Copyright   : Thodoris Papakonstantinou, 2017-2026
License     : LGPL-3
Maintainer  : dev@tpapak.com
Stability   : experimental
Portability : POSIX

Breadth-first search (BFS) for directed graphs represented as adjacency lists.
Provides two entry points:

* 'bfs' — BFS on a concrete 'Graph' value
* 'adjBFS' — BFS on an implicit graph given as an @IntMap [Vertex]@ adjacency map

Both produce a 'BFS' record containing the level (distance) of every reachable
vertex, the BFS parent map, and a topological ordering of the visited vertices.

Used by the Tide algorithm ('Data.Graph.AdjacencyList.PushRelabel.Pure') in the
@globalRelabel@ step to compute vertex heights from distances to the source and
sink in the residual graph.
 -}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}


module Data.Graph.AdjacencyList.BFS
  ( -- * BFS result
    BFS (..)
    -- * Running BFS
  , bfs
  , adjBFS
    -- * Utilities
  , spanningTree
  ) where

import Data.List
import Data.Tuple
import Data.Maybe
import qualified Data.IntMap as IM
import qualified Data.IntSet as Set

import Data.Graph.AdjacencyList

-- | Result of a breadth-first search from a single source vertex.
data BFS = BFS { frontier :: Set.IntSet
                 -- ^ Current frontier (vertices at the deepest explored level).
                 -- Empty when the search is complete.
               , level :: IM.IntMap Int
                 -- ^ Map from vertex to its BFS level (shortest distance from source).
               , parent :: IM.IntMap Vertex
                 -- ^ Map from vertex to its BFS parent.
                 -- The source vertex has no entry.
               , maxLevel :: Int
                 -- ^ Maximum level reached during the search.
               , topSort :: [Vertex]
                 -- ^ Vertices in BFS visit order.
                 -- For DAGs this coincides with a topological sort.
               } deriving (Eq, Show)

-- | Initial BFS state with only the source vertex in the frontier.
initialBFS :: Vertex -> BFS
initialBFS s = BFS { frontier = Set.singleton s
                   , level = IM.fromList [(s,0)]
                   , parent= IM.empty
                   , maxLevel = 0
                   , topSort = []
                   }

-- | Run BFS on a 'Graph' from the given source vertex.
--
-- Explores all vertices reachable from @s@ via the graph's 'neighbors'
-- function. Returns a 'BFS' record with levels, parents, and visit order.
--
-- If @s@ is not in the graph's vertex set, returns the initial (empty) BFS.
bfs :: Graph -> Vertex -> BFS
bfs g s = 
  let vset = Set.fromList (vertices g)
      sbfs = initialBFS s
      breadthFirstSearch b =
        if Set.null (frontier b) || not (Set.member s vset)
           then b { topSort = reverse (topSort b) }
           else
             let oldLevel = maxLevel b
                 newLevel = oldLevel + 1
                 oldLevels = level b
                 oldFrontiers = frontier b
                 -- Collect (neighbor, parent) pairs; use IntMap to deduplicate
                 -- and keep only newly discovered vertices in one pass
                 newParMap = Set.foldl'
                   (\acc v ->
                     foldl' (\acc' n ->
                       if IM.member n oldLevels || IM.member n acc'
                         then acc'
                         else IM.insert n v acc'
                     ) acc (neighbors g v)
                   ) IM.empty oldFrontiers
                 newFrontiers = IM.keysSet newParMap
                 newParents = IM.union (parent b) newParMap
                 newLevels = Set.foldl' 
                           (\ac v -> IM.insert v newLevel ac) 
                           oldLevels newFrontiers
                 -- Prepend frontier to topSort (reversed at the end)
                 newTopSort = Set.foldl' (flip (:)) (topSort b) oldFrontiers
                 bbfs = breadthFirstSearch (b { frontier = newFrontiers
                                              , level = newLevels 
                                              , parent = newParents
                                              , maxLevel = newLevel
                                              , topSort = newTopSort
                                            })
               in bbfs
   in breadthFirstSearch sbfs

-- | Run BFS on an implicit graph defined by an adjacency map.
--
-- @adjBFS neimap s@ performs BFS from vertex @s@ where the neighbors of
-- each vertex are given by @neimap :: IntMap [Vertex]@.  Vertices not
-- present in @neimap@ are treated as having no outgoing edges.
--
-- This is used by 'Data.Graph.AdjacencyList.PushRelabel.Internal.residualDistances'
-- to run BFS on the residual graph (whose edge set changes each tide)
-- without constructing a full 'Graph' value.
adjBFS :: IM.IntMap [Vertex] -> Vertex -> BFS
adjBFS neimap s = let b = breadthFirstSearch sbfs
                  in b { topSort = reverse (topSort b) }
  where neighbors v = case IM.lookup v neimap of
                        Nothing -> []
                        Just ns -> ns
        sbfs = initialBFS s
        breadthFirstSearch b
          | Set.null (frontier b) = b
          | otherwise = bbfs
            where oldLevel = maxLevel b
                  newLevel = oldLevel + 1
                  oldLevels = level b
                  oldFrontiers = frontier b
                  -- Collect new vertices; use IntMap to deduplicate
                  newParMap = Set.foldl'
                    (\acc v ->
                      foldl' (\acc' n ->
                        if IM.member n oldLevels || IM.member n acc'
                          then acc'
                          else IM.insert n v acc'
                      ) acc (neighbors v)
                    ) IM.empty oldFrontiers
                  newFrontiers = IM.keysSet newParMap
                  newParents = IM.union (parent b) newParMap
                  newLevels = Set.foldl' 
                                 (\ac v -> IM.insert v newLevel ac) 
                                 oldLevels newFrontiers
                  newTopSort = Set.foldl' (flip (:)) (topSort b) oldFrontiers
                  bbfs = breadthFirstSearch (b { frontier = newFrontiers
                             , level = newLevels 
                             , parent = newParents
                             , maxLevel = newLevel
                             , topSort = newTopSort
                             })

-- | Extract the BFS spanning tree as a list of edges.
--
-- Each edge @(parent, child)@ in the returned list corresponds to one
-- entry in the 'parent' map.
spanningTree :: BFS -> [Edge]
spanningTree b = 
  map (fromTuple . swap) $ IM.toList $ parent b