Skip to content

Commit 9bc219b

Browse files
committed
Add a function to compute the strongly connected components of a
`Graph`, via Tarjan's algorithm This is essentially a translation of https://stackoverflow.com/a/15912896 The stack is encoded as a `Map k Int`, and `foldl` is used extensively to obtain a *functional* presentation of the algorithm.
1 parent c4a3189 commit 9bc219b

File tree

2 files changed

+65
-1
lines changed

2 files changed

+65
-1
lines changed

Diff for: src/Data/Graph.purs

+61-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Data.Graph
2626
, isAdjacent
2727
, areConnected
2828
, shortestPath
29+
, stronglyConnectedComponents
2930
, allPaths
3031
) where
3132

@@ -42,11 +43,12 @@ import Data.List as L
4243
import Data.List as List
4344
import Data.Map (Map)
4445
import Data.Map as M
45-
import Data.Maybe (Maybe(..), isJust, maybe)
46+
import Data.Maybe (Maybe(..), fromJust, isJust, maybe)
4647
import Data.Set (Set)
4748
import Data.Set as S
4849
import Data.Set as Set
4950
import Data.Tuple (Tuple(..), fst, snd, uncurry)
51+
import Partial.Unsafe (unsafePartial)
5052

5153
-- | A graph with vertices of type `v`.
5254
-- |
@@ -271,3 +273,61 @@ topologicalSort (Graph g) =
271273
initialState = { unvisited: g
272274
, result: Nil
273275
}
276+
277+
-- | Tarjan's algorithm for Strongly Connected Components (SCCs).
278+
-- |
279+
-- | Defines a `Map` where each node is mapped to its equivalence class
280+
-- | representative once the graph is partitioned into SCCs.
281+
-- |
282+
-- | Running time: O(|E| log |V|)
283+
stronglyConnectedComponents :: forall k v. Show k => Ord k => Graph k v -> Map k k
284+
stronglyConnectedComponents (Graph g) =
285+
Foldable.foldl
286+
( \scc next ->
287+
if isJust (M.lookup next scc) then
288+
-- have already found the SCC representative of `next`
289+
scc
290+
else
291+
dfs next M.empty scc
292+
)
293+
M.empty
294+
$ M.keys g
295+
where
296+
dfs :: k -> Map k Int -> Map k k -> Map k k
297+
-- perform a depth-first search in the graph starting from `n`
298+
-- `depth` tracks the depth at which each node is visited
299+
-- `scc` is the accumulating map from each node to its representative in its
300+
-- SCC
301+
-- - when `scc` is defined on all vertices of the graph, the procedure is
302+
-- complete
303+
dfs n depth scc = case ( Foldable.foldl
304+
( \(Tuple sccAcc shallowest) c -> case M.lookup c depth of
305+
-- fold over the children `c` of `n`; the accumulator is a pair
306+
-- consisting of
307+
-- - `sccAcc`: the accumulating SCC map
308+
-- - `shallowest`: the shallowest node we have seen so far in
309+
-- this branch of the DFS (this will become the
310+
-- SCC representative)
311+
Just _ -> Tuple sccAcc $ shallower shallowest c
312+
Nothing ->
313+
let
314+
sccWithChildData = dfs c (M.insert n (M.size depth) depth) sccAcc
315+
316+
shallowestForChild = unsafePartial $ fromJust $ M.lookup c sccWithChildData
317+
-- not actually partial; `c` is guaranteed to be in the map
318+
in
319+
Tuple sccWithChildData $ shallower shallowest shallowestForChild
320+
)
321+
(Tuple scc n)
322+
$ children n (Graph g)
323+
) of
324+
Tuple newState shallow -> M.insert n shallow newState -- `shallow` becomes the representative
325+
-- of the SCC containing `n`
326+
where
327+
shallower :: k -> k -> k
328+
-- determine whether `x` or `y` was visited first in the DFS, as determined
329+
-- by `depth`
330+
shallower x y = case (Tuple (M.lookup x depth) (M.lookup y depth)) of
331+
Tuple _ Nothing -> x
332+
Tuple Nothing _ -> y
333+
Tuple (Just p) (Just q) -> if q < p then y else x

Diff for: test/Main.purs

+4
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,7 @@ main = do
130130
Graph.allPaths 1 8 acyclicGraph `shouldEqual` Set.singleton (List.fromFoldable [ 1, 2, 4, 8 ])
131131
Graph.allPaths 2 6 acyclicGraph `shouldEqual` Set.singleton (List.fromFoldable [ 2, 3, 6 ])
132132
Graph.allPaths 5 3 cyclicGraph `shouldEqual` Set.singleton (List.fromFoldable [ 5, 1, 2, 3 ])
133+
describe "stronglyConnectedComponents" do
134+
it "works for examples" do
135+
Graph.stronglyConnectedComponents acyclicGraph `shouldEqual` (Map.fromFoldable [ Tuple 1 1, Tuple 2 2, Tuple 3 3, Tuple 4 4, Tuple 5 5, Tuple 6 6, Tuple 7 7, Tuple 8 8])
136+
Graph.stronglyConnectedComponents cyclicGraph `shouldEqual` (Map.fromFoldable [ Tuple 1 1, Tuple 2 1, Tuple 3 1, Tuple 4 4, Tuple 5 5])

0 commit comments

Comments
 (0)