Introduction
For this series of posts I will cover the mathematics and computation, using Haskell, of shortest path algorithms. In the final post I will cover a topic that has piqued my interest lately, Algebraic Path Problems. First, we will begin with Dijkstra’s.
Mathematics of the Single Source Shortest Path
Here I will provide some definitions and context for the shortest path problem I will aim to calculate using Dijkstra’s.
The following is directly from Path Problems in Networks[1]:
We are given a directed graph \(G = (V, E)\) with a real-valued edge weight function \(w : E \rightarrow \mathbb{R} \cup \{\infty\}\). We will assume that all possible edges in \(V \times V\) exist, but some may have a weight of \(\infty\).
We define the weight of a path \(p = (v_0, v_1, \ldots, v_k)\) to be the sum of the weights of its edges:
\[\text{weight}(p) = \sum_{i=0}^{k-1} w(v_i, v_{i+1})\]
The shortest path weight from vertex \(u\) to vertex \(v\) is the smallest path weight among all paths that start at \(u\) and end at \(v\):
\[\delta(u, v) = \min_{p \in P_{uv}} w(p)\]
These two equations are important and will show up later in the Algebraic Path Problem.
With Dijkstra’s I will be calculating the Single Source Shortest Path Problem (SSSP). This means I have to provide the source vertex. Computationally it is faster than most algorithms that calculate multi-source search problems, with a time complexity of \(O(n^2)\).
Dijkstra’s Algorithm Pseudocode
Below is the algorithmic pseudocode for Dijkstra’s. I should note that Dijkstra’s requires non-negative weights on edges.
\(\text{Dijkstra}(G, w, s)\)
1 d[s] ← 0
2 for v ∈ V \ {s}
3 do d[v] ← ∞
4 Q ← V
5 while Q ≠ ∅
6 do u ← v s.t. d[v] = min{d[x] | x ∈ Q}
7 Q ← Q \ {u}
8 for each vertex v ∈ Q
9 do d[v] ← min{d[v], d[u] + w(u, v)}
Haskell Implementation
First, some imports and type definitions. I create a union type Distance with
a finite or infinite weight. Then I define ordinal and numeric typeclass
instances for later calculations.
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set
type Vertex = Int
type Weight = Int
data Distance = Finite Weight | Infinity
deriving (Eq, Show)
instance Ord Distance where
compare Infinity Infinity = EQ
compare Infinity _ = GT
compare _ Infinity = LT
compare (Finite a) (Finite b) = compare a b
instance Num Distance where
Finite a + Finite b = Finite (a + b)
_ + _ = Infinity
Finite a * Finite b = Finite (a * b)
_ * _ = Infinity
abs (Finite a) = Finite (abs a)
abs Infinity = Infinity
signum (Finite a) = Finite (signum a)
signum Infinity = Finite 1
fromInteger n = Finite (fromInteger n)
negate (Finite a) = Finite (negate a)
negate Infinity = Infinity
type Graph = Map.Map Vertex [(Vertex, Weight)]Next is the type signature for the function computing dijkstra’s shortest path
algorithm. I take as parameters a Graph and source Vertex and compute the
distances.
-- Given a graph and source vertex, returns shortest distances to all vertices
dijkstra :: Graph -> Vertex -> Map.Map Vertex Distance
dijkstra graph source = dijkstraHelper initialDistances unvisitedNow I will provide the initial body of the function implementation, steps 1 through 4 in the algorithm. The vertices are computed by converting the graph into a list of tuples of vertices and edges, appending those to the source vertex. I dedupe by converting to a set. Initial distances are set to infinity to indicate they have not been visited; i.e. a weight of infinity. The source gets 0 for no distance.
where
-- Step 1-4: Initialize distances and queue
vertices =
Set.fromList $
source : concatMap (\(v, edges) -> v : map fst edges) (Map.toList graph)
initialDistances =
Map.insert source (Finite 0) $
Map.fromSet (const Infinity) vertices -- d[s] ← 0, d[v] ← ∞
unvisited = vertices -- Q ← VNow I implement a helper function that I will use recursively to drain the queue, computing minimum distances along the way.
dijkstraHelper ::
Map.Map Vertex Distance ->
Set.Set Vertex ->
Map.Map Vertex Distance
dijkstraHelper distances queue
| Set.null queue = distances
-- while Q ≠ ∅
| otherwise =
let -- Step 6: u ← v s.t. d[v] = min{d[x]|x ∈ Q}
currentVertex =
-- Aggregate function to calculate delta
minimumBy
( \v1 v2 ->
compare
(Map.findWithDefault Infinity v1 distances)
(Map.findWithDefault Infinity v2 distances)
)
queue
currentDistance =
Map.findWithDefault Infinity currentVertex distances
neighbors = fromMaybe [] (Map.lookup currentVertex graph)
-- Step 9: d[v] ← min{d[v], d[u] + w(u, v)}
newDistances =
foldl (relaxEdge currentDistance) distances neighbors
-- Step 7: Q ← Q \ {u}
newQueue = Set.delete currentVertex queue
in dijkstraHelper newDistances newQueueThese are the two functions to implement edge relaxation (i.e. updating the shortest path) and minimum comparison.
relaxEdge ::
Distance ->
Map.Map Vertex Distance ->
(Vertex, Weight) ->
Map.Map Vertex Distance
relaxEdge currentDist distances (neighbor, edgeWeight) =
let newDistance = currentDist + Finite edgeWeight -- d[u] + w(u, v)
oldDistance = Map.findWithDefault Infinity neighbor distances
in if newDistance < oldDistance -- min{d[v], d[u] + w(u, v)}
then Map.insert neighbor newDistance distances
else distances
minimumBy :: (a -> a -> Ordering) -> Set.Set a -> a
minimumBy cmp set = Set.fold (\x acc -> if cmp x acc == LT then x else acc) (Set.findMin set) setIn the final version I implemented an enhanced algorithm that tracks predecessors for path reconstruction. This way I can calculate both the shortest distances from the source vertex as well as the shortest path between two vertices. With the predecessors enhancement, I account for the previous vertex I came from (our predecessor). If you would like to see the final code, check out the repo.
Conclusion
It’s always refreshing brushing up on classical algorithms like Dijkstra’s. It’s extra enjoyable for me to implement them in Haskell. I appreciate the language’s use of pattern matching and algebraic data types to structure the problem, and higher order functions allow for implementing a line of algorithmic pseudocode with a one liner that looks very similar.
Thanks for reading along. Next I will implement Bellman-Ford in Haskell. One topic I want to cover further is optimizing Dijkstra’s, so hopefully I’ll make that a future post after this series. If there are any corrections or comments please don’t hesitate to reach out.
Resources
[1] https://www.vitalsource.com/products/path-problems-in-networks-john-baras-george-v9783031799839