Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
280 changes: 280 additions & 0 deletions Data/Graph/Inductive/Query/Cycles.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
{- |
Module : Data.Graph.Inductive.Query.Cycles
Description : Algorithms for finding all cycles.
Copyright : (c) Gabriel Hjort Blindell 2017
License : 2-Clause BSD
Copy link
Contributor

Choose a reason for hiding this comment

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

FGL is licensed under 3-Clause BSD; in interests of using the same license for all, would you mind changing this?

(And some code from here is from Graphalyze, which isn't under your Copyright.)

Copy link
Author

Choose a reason for hiding this comment

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

Addressed as part of the reply to your comment below.

Copy link
Author

Choose a reason for hiding this comment

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

I decided to include a header, with the BSD3 license and copyright held by both me and you (due to copied functions from Graphalize package).

Maintainer : [email protected]
Copy link
Contributor

Choose a reason for hiding this comment

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

Will you be maintaining this module?

(Actually, if you look at all the current FGL modules they tend not to bother with the header block at all, so you could just remove the entire thing.)

Copy link
Author

Choose a reason for hiding this comment

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

Ah, I didn't read the header carefully. =)

I've removed the header entirely.

Copy link
Author

Choose a reason for hiding this comment

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

Added header, but removed maintainer information.

Defines algorithms that find all cycles in a given graph.
-}
module Data.Graph.Inductive.Query.Cycles
( cyclesIn
, cyclesIn'
, strongComponentsOf
, uniqueCycles
, uniqueCycles'
)
where

import Data.Graph.Inductive.Graph

import Data.List
Copy link
Contributor

Choose a reason for hiding this comment

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

Please put import blocks on the same line, e.g.

import Data.List ((\\), delete, tails)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

( (\\)
, delete
, tails
)
import Data.Maybe
( fromJust )
import Control.Monad
( ap )
import qualified Data.Map as M
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not an IntMap?

Copy link
Author

Choose a reason for hiding this comment

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

Ah, wasn't aware of that module. =)
Fixed.




-- | 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

-- | 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
Copy link
Contributor

Choose a reason for hiding this comment

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

This (and others) should probably be banged.

Copy link
Author

Choose a reason for hiding this comment

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

I've not used bangs before, so I'm not sure of whether they apply here or not. I'll try looking into it.

-- ^ The current index.
, sccStack :: [Node]
-- ^ The node stack.
, sccNodeInfo :: M.Map Node (Bool, Int, Int)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a specific data structure with record labels would work better than a triple here; it would not only provide documentation as to what each field means, but you can then use them as selectors rather than pattern matching on the entire triple to get values out.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

-- ^ Node information as a tuple (whether the node is on the stack, its
-- index, and its low link).
, sccGraph :: 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.

Why no instances? At least Eq, Show and Read plaese.

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.


-- | Find all strongly connected components of a graph. Implements Tarjan's
-- algorithm. Returned list is sorted in topological order.
Copy link
Contributor

Choose a reason for hiding this comment

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

More specifically Tarjan's SCC algorithm. Maybe add a link to Wikipedia or a paper?

Copy link
Author

Choose a reason for hiding this comment

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

Wiki link added.

strongComponentsOf :: (DynGraph g) => g a b -> [g a b]
strongComponentsOf g =
sccComponents $
foldr ( \n st ->
let (_, i, _) = sccNodeInfo st M.! n
in if i < 0 then findSCCFor n st else st
)
(mkInitSCCState g)
(nodes g)

findSCCFor :: (DynGraph 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 (True, i, i) (sccNodeInfo st0)
}
g = sccGraph st1
st2 = foldr ( \m st ->
Copy link
Contributor

Choose a reason for hiding this comment

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

This is large enough that I think a new function - even if it's in a where block - is warranted for documentation, etc.

That way, you can also use guards rather than nested if/else.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

let st_ni = sccNodeInfo st
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason for snake_case here?

Copy link
Author

Choose a reason for hiding this comment

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

Did some refactoring, and the only use of snake_case here is to distinguish the low link of the n node from the low link of the m node.

(m_on_stack, m_index, _) = st_ni M.! m
in if m_index < 0
then let st' = findSCCFor m st
st_ni' = sccNodeInfo st'
(n_on_stack', n_index', n_lowlink') =
st_ni' M.! n
(_, _, m_lowlink) = st_ni' M.! m
new_n_ni = ( n_on_stack'
, n_index'
, min n_lowlink' m_lowlink
)
in st' { sccNodeInfo =
M.insert n new_n_ni st_ni'
}
else if m_on_stack
then let (n_on_stack', n_index', n_lowlink') =
st_ni M.! n
new_n_ni = ( n_on_stack'
, n_index'
, min n_lowlink' m_index
)
in st { sccNodeInfo =
M.insert n new_n_ni st_ni
}
else st
)
st1
(suc g n)
(_, n_index, n_lowlink) = sccNodeInfo st2 M.! n
st3 = if n_index == n_lowlink
then let stack = sccStack st2
(p0, p1) = span (/= n) stack
comp_ns = (head p1:p0)
new_stack = tail p1
new_ni = foldr ( \n' ni ->
let (_, n_index', n_lowlink') = ni M.! n'
new_n_ni = ( False
, n_index'
, n_lowlink'
)
in M.insert n' new_n_ni ni
)
(sccNodeInfo st2)
comp_ns
comp = nfilter (`elem` comp_ns) (sccGraph st2)
new_cs = (comp:sccComponents st2)
in st2 { sccComponents = new_cs
, sccStack = new_stack
, sccNodeInfo = new_ni
}
else st2
in st3

mkInitSCCState :: (DynGraph g) => g a b -> SCCState g a b
mkInitSCCState g =
let ns = nodes g
in SCCState { sccComponents = []
, sccCurrentIndex = 0
, sccStack = []
, sccNodeInfo = M.fromList $ zip ns (repeat (False, -1, -1))
, sccGraph = g
}

-- | Contains the necessary data structures used by 'cyclesIn'.
data CyclesInState g a b
= CyclesInState
{ cisCycles :: [[Node]]
-- ^ The cycles found so far, in topological order.
, cisBlocked :: M.Map Node Bool
-- ^ The nodes which are currently blocked.
, cisBlockMap :: M.Map Node [Node]
-- ^ The B set.
, cisStack :: [Node]
-- ^ The node stack.
, cisS :: Maybe Node
-- ^ The current S value.
, cisCurrentComp :: Maybe (g a b)
-- ^ The component currently being processed.
, 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.


-- | Finds all cycles in a given graph 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.
cyclesIn :: (DynGraph g) => g a b -> [[LNode a]]
cyclesIn g = map (addLabels g) (cyclesIn' g)

-- | Finds all cycles in a given graph 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.
cyclesIn' :: (DynGraph g) => g a b -> [[Node]]
cyclesIn' g =
cisCycles $
foldr cyclesFor (mkInitCyclesInState g) (nodes g)

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

-- | Find all cycles in the given graph, excluding those that are also cliques.
uniqueCycles' :: (DynGraph g) => g a b -> [[Node]]
uniqueCycles' g = filter (not . isRegular g) (cyclesIn' g)

cyclesFor :: (DynGraph 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 = st0 { cisS = Just n
, cisCurrentComp = Just n_comp
}
st2 = fst $ cCircuits n st1
g = cisGraph st2
new_g = delNode n g
new_comps = strongComponentsOf new_g
st3 = st2 { cisGraph = new_g
, cisComponents = new_comps
}
in st3
else st0 -- Skip to next node

cCircuits :: (DynGraph g) => Node -> CyclesInState g a b ->
(CyclesInState g a b, Bool)
cCircuits n st0 =
let st1 = st0 { cisBlocked = M.insert n True (cisBlocked st0)
, cisStack = (n:cisStack st0)
}
c = fromJust $ cisCurrentComp st1
n_suc = suc c n
(st2, f) =
foldr ( \m (st, f') ->
Copy link
Contributor

Choose a reason for hiding this comment

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

This is big enough that it should be split into a separate function.

Copy link
Author

Choose a reason for hiding this comment

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

Now broken into several, smaller functions.

if m == fromJust (cisS st)
then let new_cycle = reverse (m:cisStack st)
st' = st { cisCycles = (new_cycle:cisCycles st) }
in (st', True)
else if not (cisBlocked st M.! m)
then let (st', f'') = cCircuits m st
in (st', f' || f'')
else (st, f')
)
(st1, False)
n_suc
st3 = if f
then cUnblock n st2
else foldr ( \m st ->
let bm = cisBlockMap st
m_blocked = bm M.! m
new_m_blocked = (n:m_blocked)
in if n `notElem` m_blocked
then st { cisBlockMap =
M.insert m new_m_blocked bm
}
else st
)
st2
n_suc
st4 = st3 { cisStack = tail $ cisStack st3 }
in (st4, f)

cUnblock :: (DynGraph 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 :: (DynGraph 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 = []
, cisS = Nothing
, cisCurrentComp = Nothing
, cisComponents = strongComponentsOf g
, cisGraph = g
}
1 change: 1 addition & 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 Down