Skip to content

Commit 6cc57f9

Browse files
author
Abhijit Sarkar
committed
Extract Graph module
1 parent 2571207 commit 6cc57f9

33 files changed

+906
-721
lines changed

ninety-nine-haskell.cabal

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,18 @@ library
5454
BinaryTree.P68
5555
BinaryTree.P69
5656
DList
57-
Graphs
57+
Graph.Graph
58+
Graph.P80
59+
Graph.P81
60+
Graph.P82
61+
Graph.P83
62+
Graph.P84
63+
Graph.P85
64+
Graph.P86
65+
Graph.P87
66+
Graph.P88
67+
Graph.P89
68+
Graph.Search
5869
List.P01
5970
List.P02
6071
List.P03
@@ -102,7 +113,12 @@ library
102113
Misc.P92
103114
Misc.P93
104115
Misc2
105-
Monads
116+
Monad.P74
117+
Monad.P75
118+
Monad.P76
119+
Monad.P77
120+
Monad.P78
121+
Monad.P79
106122
MultiwayTree.MultiwayTree
107123
MultiwayTree.P70
108124
MultiwayTree.P70b
@@ -163,7 +179,15 @@ test-suite ninety-nine-test
163179
BinaryTree.P68Spec
164180
BinaryTree.P69Spec
165181
BinaryTree.Trees
166-
GraphsSpec
182+
Graph.P81Spec
183+
Graph.P82Spec
184+
Graph.P83Spec
185+
Graph.P84Spec
186+
Graph.P85Spec
187+
Graph.P86Spec
188+
Graph.P87Spec
189+
Graph.P88Spec
190+
Graph.P89Spec
167191
List.GenList
168192
List.P01Spec
169193
List.P02Spec
@@ -204,7 +228,9 @@ test-suite ninety-nine-test
204228
Misc.P92Spec
205229
Misc.P93Spec
206230
Misc2Spec
207-
MonadsSpec
231+
Monad.P77Spec
232+
Monad.P78Spec
233+
Monad.P79Spec
208234
MultiwayTree.GenMultiwayTree
209235
MultiwayTree.P70cSpec
210236
MultiwayTree.P70Spec

src/Graph/Graph.hs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module Graph.Graph where
2+
3+
import Control.Monad ((<=<))
4+
import qualified Data.List as L
5+
import Data.Map (Map)
6+
import qualified Data.Map as Map
7+
8+
-------------------------------------------------------------------------
9+
-- -
10+
-- Graph utilities
11+
-- -
12+
-------------------------------------------------------------------------
13+
type Graph a = Map a [a]
14+
15+
type Edge a = (a, a)
16+
17+
-- Builds a directed graph.
18+
buildG :: (Ord a) => [Edge a] -> Graph a
19+
buildG = foldr merge Map.empty
20+
where
21+
-- insertWith called with key, f new_value old_value,
22+
-- and new_value is a singleton.
23+
merge (u, v) = Map.insertWith ((:) . head) u [v]
24+
25+
-- Reverses the edges.
26+
reverseE :: [Edge a] -> [Edge a]
27+
reverseE = map (\(u, v) -> (v, u))
28+
29+
-- Builds an undirected graph.
30+
-- Since the graph may contain cycles, make sure
31+
-- to not include the same vertex more than once.
32+
buildUG :: (Ord a) => [Edge a] -> Graph a
33+
buildUG edges = Map.unionWith ((L.nub .) . (++)) g g'
34+
where
35+
g = buildG edges
36+
g' = (buildG . reverseE) edges
37+
38+
vertices :: (Eq a) => Graph a -> [a]
39+
vertices = L.nub . (uncurry (:) <=< Map.toList)
40+
41+
neighbors :: (Ord a) => Graph a -> a -> [a]
42+
neighbors = flip (Map.findWithDefault [])

src/Graph/P80.hs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Graph.P80 where
2+
3+
{-
4+
Problem 80: (***) Conversions.
5+
6+
Write predicates to convert between the different graph representations.
7+
With these predicates, all representations are equivalent; i.e. for the
8+
following problems you can always pick freely the most convenient form.
9+
The reason this problem is rated (***) is not because it's particularly
10+
difficult, but because it's a lot of work to deal with all the special cases.
11+
12+
ANSWER: TODO.
13+
-}

src/Graph/P81.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
3+
module Graph.P81 where
4+
5+
import qualified Data.Set as Set
6+
import Graph.Graph
7+
import Graph.Search
8+
9+
-- Problem 81: (**) Paths between two given nodes.
10+
paths :: (Ord a) => a -> a -> [Edge a] -> [[a]]
11+
paths start end edges = search Search {..}
12+
where
13+
ug = buildUG edges
14+
expand visited = filter (`Set.notMember` visited) . neighbors ug
15+
isDone = const (end ==)

src/Graph/P82.hs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
3+
module Graph.P82 where
4+
5+
import qualified Data.Set as Set
6+
import Graph.Graph
7+
import Graph.Search
8+
import Prelude hiding (cycle)
9+
10+
{-
11+
Problem 82: (*) Cycle from a given node.
12+
13+
Write a predicate cycle to find a closed path (cycle) P starting at a
14+
given node A in the graph G. The predicate should return all cycles
15+
via backtracking.
16+
-}
17+
cycle :: (Ord a) => a -> [Edge a] -> [[a]]
18+
-- Filter out trivial cycles like 1-2-1.
19+
cycle start edges = filter ((> 3) . length) cycles
20+
where
21+
cycles = search Search {..}
22+
ug = buildUG edges
23+
expand visited =
24+
filter (\v -> v == start || v `Set.notMember` visited)
25+
. neighbors ug
26+
-- Since we start with the start vertex, we need to
27+
-- make sure that the search doesn't terminate immediately.
28+
isDone visited u = u == start && (not . Set.null) visited

src/Graph/P83.hs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
3+
module Graph.P83 where
4+
5+
import qualified Data.Map as Map
6+
import qualified Data.Set as Set
7+
import Graph.Graph
8+
import Graph.Search
9+
10+
{-
11+
Problem 83: (**) Construct all spanning trees.
12+
-}
13+
spanningTrees :: (Ord a) => [Edge a] -> [[a]]
14+
spanningTrees edges = concatMap go $ vertices ug
15+
where
16+
go start = search Search {..}
17+
ug = buildUG edges
18+
expand visited = filter (`Set.notMember` visited) . neighbors ug
19+
isDone visited _ = 1 + Set.size visited == Map.size ug

src/Graph/P84.hs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
3+
module Graph.P84 where
4+
5+
import qualified Control.Monad.State as S
6+
import qualified Data.HashPSQ as Q
7+
import Data.Hashable (Hashable)
8+
import qualified Data.Set as Set
9+
10+
{-
11+
Problem 84: (**) Construct the minimal spanning trees.
12+
13+
ANSWER: We use Prim's eager MST algorithm.
14+
15+
https://www.youtube.com/watch?v=xq3ABa-px_g
16+
17+
- Maintain a min Indexed Priority Queue (IPQ) of size V
18+
that sorts vertex-edge pairs based on the min edge cost
19+
of e. By default, all vertices v have a best value of ∞
20+
in the IPQ.
21+
22+
- Start the algorithm on any node 's'. Mark s as visited
23+
and relax all edges of s.
24+
Relaxing refers to updating the entry for node v in the
25+
IPQ from (v, old_edge) to (v, new_edge) if the new_edge
26+
from u -> v has a lower cost than old_edge.
27+
28+
- While the IPQ is not empty and a MST has not been formed,
29+
deque the next best (v, e) pair from the IPQ. Mark node v
30+
as visited and add edge e to the MST.
31+
32+
- Next, relax all edges of v while making sure not to relax
33+
any edge pointing to a node which has already been visited.
34+
35+
This algorithm runs in O(E log V) time since there can only
36+
be V (node, edge) pairs in the IPQ, making the update and
37+
poll operations O(log V).
38+
-}
39+
prim :: (Ord a, Hashable a) => [(a, a, Int)] -> [(a, a, Int)]
40+
prim edges = S.evalState go initialState
41+
where
42+
(u0, _, _) = head edges
43+
-- Start with all edges incident to u0 on the heap.
44+
initialState = (Set.singleton u0, relax Q.empty (outE u0))
45+
-- Sorts the given edge so that vertex u appears first.
46+
sortE u (x, y, cost) = if x == u then (x, y, cost) else (y, x, cost)
47+
-- Determines if the given edge is incident to u.
48+
isIncidentTo u (x, y, _) = x == u || y == u
49+
-- Finds all edges incident to u, and sorts them so that u appears first.
50+
outE u = (map (sortE u) . filter (isIncidentTo u)) edges
51+
-- Relaxes the given edges.
52+
relax = foldl update
53+
-- If the edge (from-to) is not present on the heap, inserts it,
54+
-- otherwise if the edge on the heap has a greater cost,
55+
-- replaces it with the given edge.
56+
update q (from, to, cost) =
57+
snd $
58+
Q.alter
59+
( \case
60+
Nothing -> ((), Just (cost, from))
61+
Just (p, v) ->
62+
if p <= cost
63+
then ((), Just (p, v))
64+
else ((), Just (cost, from))
65+
)
66+
to
67+
q
68+
go = do
69+
(visited, q) <- S.get
70+
-- Edges are put on the queue as v: (u, priority),
71+
-- where the edge is incident to the vertex 'v'
72+
-- from the vertex 'u'. The edge cost is the priority.
73+
case Q.minView q of
74+
Nothing -> return []
75+
-- At each iteration, pick an edge (v, u) with the minimum cost,
76+
-- and relax all edges incident to v, except for those connected
77+
-- to vertices already visited.
78+
Just (to, cost, from, rest) -> do
79+
let out = filter (\(_, v, _) -> v `Set.notMember` visited) $ outE to
80+
let q' = relax rest out
81+
let visited' = Set.insert to visited
82+
S.put (visited', q')
83+
xs <- go
84+
return $ (from, to, cost) : xs

src/Graph/P85.hs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
module Graph.P85 where
2+
3+
import qualified Data.List as L
4+
import qualified Data.Map as Map
5+
import qualified Data.Maybe as Mb
6+
import Graph.Graph
7+
8+
{-
9+
Problem 85: (**) Graph isomorphism.
10+
11+
Two graphs G1(N1,E1) and G2(N2,E2) are isomorphic if there is a bijection
12+
f: N1 -> N2 such that for any nodes X,Y of N1, X and Y are adjacent
13+
if and only if f(X) and f(Y) are adjacent.
14+
15+
Write a predicate that determines whether two graphs are isomorphic.
16+
17+
ANSWER:
18+
We apply the Weisfeiler Leman graph isomorphism test.
19+
20+
https://davidbieber.com/post/2019-05-10-weisfeiler-lehman-isomorphism-test/
21+
22+
https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
23+
24+
- At each iteration, the algorithm assigns to each node a tuple
25+
containing the node's old compressed label and a list of the
26+
node's neighbors' compressed labels. This is the node's new
27+
"uncompressed" label.
28+
- The algorithm then groups uncompressed labels and assign a unique
29+
id to each group that is the "compressed" label for that group.
30+
- If the number of groups is the same as the number of groups in the
31+
previous iteration, the algorithm does the following:
32+
- The compressed labels are reduced to a "canonical" form which is
33+
a sorted list of tuples of the form (label, count).
34+
- If two graphs have the same canonical form, they may be isomorphic.
35+
If not, they are certainly not isomorphic.
36+
- If the number of groups is not the same, the algorithm assigns compressed
37+
labels to each node and continues to the next iteration.
38+
Any two nodes with the same uncompressed label will get the same
39+
compressed label.
40+
41+
- The algorithm starts by assigning each node the same compressed label, 0.
42+
- One possible convention for creating compressed labels is to use increasing
43+
integers starting from 1.
44+
45+
The core idea of the Weisfeiler-Lehman isomorphism test is to find for each
46+
node in each graph a signature based on the neighborhood around the node.
47+
These signatures can then be used to find the correspondance between nodes
48+
in the two graphs, which can be used to check for isomorphism.
49+
50+
In the algorithm descibed above, the "compressed labels" serve as the signatures.
51+
-}
52+
iso :: (Ord a, Ord b) => [a] -> [Edge a] -> [b] -> [Edge b] -> Bool
53+
iso v1 e1 v2 e2 = m == n && go 0 0 (map (,0) v1) (map (,0) v2) 1
54+
where
55+
ug1 = Map.unionWith (++) (Map.fromList $ map (,[]) v1) (buildUG e1)
56+
ug2 = Map.unionWith (++) (Map.fromList $ map (,[]) v2) (buildUG e2)
57+
m = length v1
58+
n = length v2
59+
60+
-- Finds old label.
61+
label :: (Eq a) => [(a, Int)] -> a -> Int
62+
label cl = Mb.fromJust . flip L.lookup cl
63+
-- Given the neighbors and their compressed labels,
64+
-- computes new uncompressed label for this vertex.
65+
uncompress :: (Eq a) => [(a, Int)] -> [a] -> [Int]
66+
uncompress cl = L.sort . map (label cl)
67+
-- Groups uncompressed labels, and
68+
-- assigns a label to each group.
69+
group :: [(Int, [Int])] -> Int -> [((Int, [Int]), (Int, Int))]
70+
group ucl labelId =
71+
zipWith
72+
(\xs k -> (head xs, (length xs, k)))
73+
(L.group $ L.sort ucl)
74+
[labelId + 1 ..]
75+
-- Replaces each uncompressed group with its compressed label.
76+
compress :: [(Int, [Int])] -> [((Int, [Int]), (Int, Int))] -> [Int]
77+
compress ucl groups = map (snd . Mb.fromJust . flip L.lookup groups) ucl
78+
-- Reduces the graph into canonical form.
79+
canonical :: [((Int, [Int]), (Int, Int))] -> [(Int, Int)]
80+
canonical = L.sortOn fst . map (\(_, (x, y)) -> (y, x))
81+
82+
-- go :: Int -> Int -> [(a, Int)] -> [(b, Int)] -> Int -> Bool
83+
go i labelId cl1 cl2 numLabels
84+
| i == n = False
85+
| otherwise = do
86+
-- Create uncompressed labels.
87+
let ucl1 =
88+
zipWith ((,) . snd) cl1 $
89+
map (uncompress cl1 . neighbors ug1) v1
90+
let ucl2 =
91+
zipWith ((,) . snd) cl2 $
92+
map (uncompress cl2 . neighbors ug2) v2
93+
94+
-- Reduce uncompressed labels to compressed labels.
95+
let grp1 = group ucl1 labelId
96+
let grp2 = group ucl2 labelId
97+
98+
let k = length grp1
99+
100+
if length grp2 == k && numLabels == k
101+
then do
102+
-- Create the canonical graphs.
103+
let c1 = canonical grp1
104+
let c2 = canonical grp2
105+
c1 == c2
106+
else do
107+
-- Assign compressed labels.
108+
let cl1' = zip v1 (compress ucl1 grp1)
109+
let cl2' = zip v2 (compress ucl2 grp2)
110+
go (i + 1) (labelId + k) cl1' cl2' k

0 commit comments

Comments
 (0)