Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added algorithms for finding strong components and cycles. #65

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5996a24
Added algorithms for finding strong components and cycles.
May 3, 2017
ca867a9
Removed header.
May 4, 2017
205af43
Added comment about functions copied from other packages.
May 4, 2017
b9019fe
Put import blocks on the same line, according to coding convention.
May 4, 2017
6ce6e5d
Replaced use of Map with IntMap.
May 4, 2017
73707fe
Replaced use of tuple with a new data type.
May 4, 2017
6d51e8b
Added instances to data types.
May 4, 2017
003c96c
Added wiki link to Tarjan's SCC algorithm.
May 4, 2017
c8649fa
Broken out large anonymous functions into named functions.
May 4, 2017
ab5064a
Put short if-then-else statement on the same line.
May 4, 2017
514151b
Code refactoring.
May 4, 2017
f2e043e
Broken out SCC parts from Cycles into its own module. Also renamed 'c…
May 4, 2017
8e4102c
Added header.
May 4, 2017
874da10
Cycles and SCC modules no longer require the graphs to be of DynGraph…
May 4, 2017
73694ef
Added tests for SCC module.
May 4, 2017
a8b439b
Added tests for the Cycles module.
May 4, 2017
ed7df27
Improved documentation. Lists returned from cycles no longer contains…
May 4, 2017
112309d
Fixed bug.
May 4, 2017
0dc411a
Fixed test case.
May 4, 2017
bd818d2
Broken down cCircuits into several smaller functions.
Oct 3, 2017
99948d3
Fixed erronous function descriptions of cycles' and uniqueCycles'.
Oct 3, 2017
766a4f2
Added missing bang patterns to CyclesInState.
Oct 3, 2017
3ce62ca
Added missing bang patterns to SCCState.
Oct 3, 2017
c06f25c
Fixed typo.
Oct 3, 2017
a60460e
Expanded test_strongComponentsOf to check that there exists no cycle …
Oct 3, 2017
0fd7130
Rewrote cCircuitsVisit using guards.
Oct 3, 2017
d3e2a0f
Removed cisS field from CyclesInState, and now passing that value as …
Oct 3, 2017
77e2d30
Removed cisCurrentComp field from CyclesInState, and now passing that…
Oct 3, 2017
4dd06b7
Replaced use of noNodes with another method that only takes O(1) inst…
Oct 3, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Data/Graph/Inductive/Query.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Data.Graph.Inductive.Query (module Q) where
import Data.Graph.Inductive.Query.ArtPoint as Q
import Data.Graph.Inductive.Query.BCC as Q
import Data.Graph.Inductive.Query.BFS as Q
import Data.Graph.Inductive.Query.Cycles as Q
import Data.Graph.Inductive.Query.DFS as Q
import Data.Graph.Inductive.Query.Dominators as Q
import Data.Graph.Inductive.Query.GVD as Q
Expand All @@ -11,5 +12,6 @@ import Data.Graph.Inductive.Query.MaxFlow as Q
import Data.Graph.Inductive.Query.MaxFlow2 as Q
import Data.Graph.Inductive.Query.Monad as Q
import Data.Graph.Inductive.Query.MST as Q
import Data.Graph.Inductive.Query.SCC as Q
import Data.Graph.Inductive.Query.SP as Q
import Data.Graph.Inductive.Query.TransClos as Q
167 changes: 167 additions & 0 deletions Data/Graph/Inductive/Query/Cycles.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{- |
Module : Data.Graph.Inductive.Query.Cycles
Description : Finds all cycles.
Copyright : (c) Gabriel Hjort Blindell <[email protected]>
Ivan Lazar Miljenovic <[email protected]>
License : BSD3
-}

module Data.Graph.Inductive.Query.Cycles
( cycles
, cycles'
, uniqueCycles
, uniqueCycles'
)
where

import Data.Graph.Inductive.Graph
import Data.Graph.Inductive.Query.SCC

import Data.List ((\\), delete, tails)
import Data.Maybe (fromJust)
import Control.Monad (ap)
import qualified Data.IntMap as M



-- The following functions were copied from the Graphalyze package.

-- | Obtain the labels for a list of 'Node's. It is assumed that each 'Node' is
-- indeed present in the given graph.
addLabels :: (Graph g) => g a b -> [Node] -> [LNode a]
addLabels gr = map (ap (,) (fromJust . lab gr))

twoCycle :: (Graph g) => g a b -> Node -> [Node]
twoCycle gr n = filter (elem n . suc gr) (delete n $ suc gr n)

-- | Determines if the list of nodes represents a regular subgraph.
isRegular :: (Graph g) => g a b -> [Node] -> Bool
isRegular g ns = all allTwoCycle split
where
-- Node + Rest of list
split = zip ns tns'
tns' = tail $ tails ns
allTwoCycle (n,rs) = null $ rs \\ twoCycle g n

-- End of copied functions.



-- | Contains the necessary data structures used by 'cycles'.
data CyclesInState g a b
= CyclesInState
{ cisCycles :: ![[Node]]
-- ^ The cycles found so far, in topological order.
, cisBlocked :: !(M.IntMap Bool)
-- ^ The nodes which are currently blocked.
, cisBlockMap :: !(M.IntMap [Node])
-- ^ The B set.
, cisStack :: ![Node]
-- ^ The node stack.
, cisComponents :: ![g a b]
-- ^ The components of the input graph.
, cisGraph :: !(g a b)
-- ^ The input graph.
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instances please

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show, Read, and Eq added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needs bangs: ![[Node]], !(M.IntMap Bool), etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bang patterns added.

deriving (Show, Read, Eq)

-- | Finds all cycles in a given graph. The returned lists contains the nodes
-- appearing in the cycles, in successor order (although it is undefined which
-- node appears first).
--
-- Implemented using Johnson's algorithm. See Donald B. Johnson: Finding All the
-- Elementary Circuits of a Directed Graph. SIAM Journal on Computing. Volumne
-- 4, Nr. 1 (1975), pp. 77-84.
cycles :: (Graph g) => g a b -> [[LNode a]]
cycles g = map (addLabels g) (cycles' g)

-- | Same as 'cycles' but does not return the node labels.
cycles' :: (Graph g) => g a b -> [[Node]]
cycles' g =
cisCycles $
foldr cyclesFor (mkInitCyclesInState g) (nodes g)

-- | Find all cycles in the given graph (using 'cycles'), excluding those that
-- are also cliques.
uniqueCycles :: (Graph g) => g a b -> [[LNode a]]
uniqueCycles g = map (addLabels g) (uniqueCycles' g)

-- | Same as 'uniqueCycles' but does not return the node labels.
uniqueCycles' :: (Graph g) => g a b -> [[Node]]
uniqueCycles' g = filter (not . isRegular g) (cycles' g)

cyclesFor :: (Graph g) => Node -> CyclesInState g a b -> CyclesInState g a b
cyclesFor n st0 =
let n_comp = head $
filter (\c -> n `gelem` c) $
cisComponents st0
in if noNodes n_comp > 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that noNodes is O(|V|) for PatriciaTree (but O(1) for Tree); it would probably be better to do something like not . null . drop 1 . nodes $ n_comp which should be O(1).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, didn't know that. Fixed.

then let st1 = fst $ cCircuits n_comp n n st0
g = cisGraph st1
new_g = delNode n g
new_comps = strongComponentsOf new_g
st2 = st1 { cisGraph = new_g
, cisComponents = new_comps
}
in st2
else st0 -- Skip to next node

cCircuits :: (Graph g) => (g a b) -> Node -> Node -> CyclesInState g a b ->
(CyclesInState g a b, Bool)
cCircuits c s n st0 =
let st1 = st0 { cisBlocked = M.insert n True (cisBlocked st0)
, cisStack = (n:cisStack st0)
}
n_suc = suc c n
(st2, f) = foldr (cCircuitsVisit c s) (st1, False) n_suc
st3 = if f
then cUnblock n st2
else foldr (cCircuitsBlock n) st2 n_suc
st4 = st3 { cisStack = tail $ cisStack st3 }
in (st4, f)

cCircuitsVisit :: (Graph g) => (g a b) -> Node -> Node ->
(CyclesInState g a b, Bool) -> (CyclesInState g a b, Bool)
cCircuitsVisit c s n (st0, f0)
| n == s =
let new_cycle = reverse $ cisStack st0
st1 = st0 { cisCycles = (new_cycle:cisCycles st0) }
in (st1, True)
| not (cisBlocked st0 M.! n) =
let (st1, f1) = cCircuits c s n st0
in (st1, f0 || f1)
| otherwise = (st0, f0)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could have been in a where clause rather than top level.

Maybe consider guards rather than nested if/then/else?

The fromJust can also potentially throw an error. Using PatternGuards can make that neater.

cCircuitsVisit n (st0, f0)
  | maybe False (n==) (cisS st0) = ...
  | fromMaybe False (M.lookup n (cisBlocked st0)) = ...
  | otherwise = ...

(Or else use case (cisS st0, M.lookup n (cisBlocked st0)) of (Just n', _) | n == n' -> ...; (_, Just True) ...).

Same goes with the other functions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now using guards and removed need for fromJust.

cCircuitsBlock :: (Graph g) => Node -> Node -> CyclesInState g a b ->
CyclesInState g a b
cCircuitsBlock n m st0 =
let bm = cisBlockMap st0
m_blocked = bm M.! m
new_m_blocked = (n:m_blocked)
in if n `notElem` m_blocked
then st0 { cisBlockMap = M.insert m new_m_blocked bm }
else st0

cUnblock :: (Graph g) => Node -> CyclesInState g a b -> CyclesInState g a b
cUnblock n st0 =
let n_blocked = cisBlockMap st0 M.! n
st1 = st0 { cisBlocked = M.insert n False (cisBlocked st0)
, cisBlockMap = M.insert n [] (cisBlockMap st0)
}
st2 = foldr ( \m st ->
if cisBlocked st M.! m then cUnblock m st else st
)
st1
n_blocked
in st2

mkInitCyclesInState :: (Graph g) => g a b -> CyclesInState g a b
mkInitCyclesInState g =
let ns = nodes g
in CyclesInState { cisCycles = []
, cisBlocked = M.fromList $ zip ns (repeat False)
, cisBlockMap = M.fromList $ zip ns (repeat [])
, cisStack = []
, cisComponents = strongComponentsOf g
, cisGraph = g
}
128 changes: 128 additions & 0 deletions Data/Graph/Inductive/Query/SCC.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{- |
Module : Data.Graph.Inductive.Query.SCC
Description : Finds all strongly connected components.
Copyright : (c) Gabriel Hjort Blindell <[email protected]>
License : BSD3
-}

module Data.Graph.Inductive.Query.SCC
( strongComponentsOf )
where

import Data.Graph.Inductive.Graph

import qualified Data.IntMap as M



-- | Node information (whether the node is on the stack, its index, and its low
-- link), which is used as part of 'SCCState'.
data SCCNodeInfo
= SCCNodeInfo
{ sccIsNodeOnStack :: Bool
, sccNodeIndex :: Int
, sccNodeLowLink :: Int
}
deriving (Show, Read, Eq)

-- | Contains the necessary data structures used by 'strongComponentsOf'.
data SCCState g a b
= SCCState
{ sccComponents :: !([g a b])
-- ^ The components found so far.
, sccCurrentIndex :: !Int
-- ^ The current index.
, sccStack :: ![Node]
-- ^ The node stack.
, sccNodeInfo :: !(M.IntMap SCCNodeInfo)
-- ^ Node information.
, sccGraph :: !(g a b)
-- ^ The input graph.
}
deriving (Show, Read, Eq)

-- | Find all strongly connected components of a graph. Returned list is sorted
-- in topological order.
--
-- Implements Tarjan's algorithm:
-- https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
strongComponentsOf :: (Graph g) => g a b -> [g a b]
strongComponentsOf g =
sccComponents $
foldr ( \n st ->
let i = sccNodeIndex $ sccNodeInfo st M.! n
in if i < 0 then findSCCFor n st else st
)
(mkInitSCCState g)
(nodes g)

findSCCFor :: (Graph g) => Node -> SCCState g a b -> SCCState g a b
findSCCFor n st0 =
let i = sccCurrentIndex st0
st1 = st0 { sccCurrentIndex = i + 1
, sccStack = (n:sccStack st0)
, sccNodeInfo = M.insert n
(SCCNodeInfo True i i)
(sccNodeInfo st0)
}
g = sccGraph st1
st2 = foldr computeLowLinks st1 (suc g n)
ni = sccNodeInfo st2 M.! n
index = sccNodeIndex ni
lowlink = sccNodeLowLink ni
st3 = if index == lowlink then produceSCC st2 else st2
in st3
where
computeLowLinks m st
| isIndexUndefined =
let st' = findSCCFor m st
ni = sccNodeInfo st' M.! n
n_lowlink = sccNodeLowLink ni
m_lowlink = sccNodeLowLink $ sccNodeInfo st' M.! m
new_ni = ni { sccNodeLowLink = min n_lowlink m_lowlink }
in st' { sccNodeInfo = M.insert n new_ni (sccNodeInfo st') }
| isOnStack =
let ni = sccNodeInfo st M.! n
n_lowlink = sccNodeLowLink ni
m_index = sccNodeIndex $ sccNodeInfo st M.! m
new_ni = ni { sccNodeLowLink = min n_lowlink m_index }
in st { sccNodeInfo = M.insert n new_ni (sccNodeInfo st) }
| otherwise = st
where isIndexUndefined = let i = sccNodeIndex $ (sccNodeInfo st) M.! m
in i < 0
isOnStack = sccIsNodeOnStack $ (sccNodeInfo st) M.! m
produceSCC st =
let stack = sccStack st
(p0, p1) = span (/= n) stack
ns = (head p1:p0)
lab_ns = filter (\(n', _) -> n' `elem` ns) $
labNodes $
sccGraph st
new_stack = tail p1
new_map = foldr ( \n' ni_map ->
let ni = ni_map M.! n'
new_ni = ni { sccIsNodeOnStack = False }
in M.insert n' new_ni ni_map
)
(sccNodeInfo st)
ns
lab_es = filter (\(n', m', _) -> n' `elem` ns && m' `elem` ns) $
labEdges $
sccGraph st
comp = mkGraph lab_ns lab_es
new_cs = (comp:sccComponents st)
in st { sccComponents = new_cs
, sccStack = new_stack
, sccNodeInfo = new_map
}

mkInitSCCState :: (Graph g) => g a b -> SCCState g a b
mkInitSCCState g =
let ns = nodes g
init_ni = SCCNodeInfo False (-1) (-1)
in SCCState { sccComponents = []
, sccCurrentIndex = 0
, sccStack = []
, sccNodeInfo = M.fromList $ zip ns (repeat init_ni)
, sccGraph = g
}
2 changes: 1 addition & 1 deletion Data/Graph/Inductive/Query/SP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ spLength s t = getDistance t . spTree s

-- | Shortest path between two nodes, if any.
--
-- Returns 'Nothing' if the destination is not reachable from teh
-- Returns 'Nothing' if the destination is not reachable from the
-- start node, and @'Just' <path>@ otherwise.
--
-- The edge labels of type @b@ are the edge weights; negative edge
Expand Down
2 changes: 2 additions & 0 deletions fgl.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ library {
Data.Graph.Inductive.Query.ArtPoint,
Data.Graph.Inductive.Query.BCC,
Data.Graph.Inductive.Query.BFS,
Data.Graph.Inductive.Query.Cycles,
Data.Graph.Inductive.Query.DFS,
Data.Graph.Inductive.Query.Dominators,
Data.Graph.Inductive.Query.GVD,
Expand All @@ -58,6 +59,7 @@ library {
Data.Graph.Inductive.Query.MaxFlow,
Data.Graph.Inductive.Query.MaxFlow2,
Data.Graph.Inductive.Query.Monad,
Data.Graph.Inductive.Query.SCC,
Data.Graph.Inductive.Query.SP,
Data.Graph.Inductive.Query.TransClos,
Data.Graph.Inductive
Expand Down
Loading