ExamplesBy LevelBy TopicLearning Paths
100 Advanced

Dijkstra's Shortest Path

Graph AlgorithmsPriority QueuesFunctional Data Structures

Tutorial Video

Text description (accessibility)

This video demonstrates the "Dijkstra's Shortest Path" functional Rust example. Difficulty level: Advanced. Key concepts covered: Graph Algorithms, Priority Queues, Functional Data Structures. Given a weighted directed graph and a source node, find the shortest distance from the source to every reachable node. Key difference from OCaml: 1. **Mutability:** OCaml's `IntMap` is truly immutable (new map per update). Rust's `HashMap` is mutated in place but owned — Rust's type system prevents the bugs that mutation usually causes.

Tutorial

The Problem

Given a weighted directed graph and a source node, find the shortest distance from the source to every reachable node. This is Dijkstra's algorithm — one of the most important algorithms in computer science — implemented with functional programming patterns in both OCaml and Rust.

🎯 Learning Outcomes

  • • Implement graph algorithms using functional patterns (folds, immutable maps)
  • • Understand how ownership in Rust prevents aliasing bugs common in mutable graph code
  • • Compare functional priority queues (OCaml sorted list) vs Rust's BinaryHeap (reversed for min-heap)
  • • Practice pattern matching on complex data structures
  • • Build path reconstruction through backtracking on distance maps
  • 🦀 The Rust Way

    Rust uses BinaryHeap (reversed via custom Ord for min-heap behavior) and HashMap for distances. While the maps are technically mutable, the algorithm follows a functional accumulation pattern:

    while let Some(State { cost, node }) = heap.pop() {
        if cost > *dist.get(&node).unwrap_or(&u64::MAX) {
            continue; // skip stale — functional "already visited"
        }
        // fold over neighbors...
    }
    

    Key features:

  • Ownership prevents aliasing — no accidental shared references to graph nodes
  • Custom Ord — Rust's max-heap becomes min-heap via reversed comparison
  • Zero-cost abstractions — iterator chains compile to the same code as manual loops
  • Path reconstruction — functional backtracking through the distance map
  • Code Example

    #[derive(Clone, Eq, PartialEq)]
    struct State { cost: u64, node: usize }
    
    impl Ord for State {
        fn cmp(&self, other: &Self) -> Ordering {
            // Reverse ordering: BinaryHeap is max-heap, we want min-heap
            other.cost.cmp(&self.cost).then_with(|| self.node.cmp(&other.node))
        }
    }
    impl PartialOrd for State {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
    }
    
    pub fn dijkstra(graph: &Graph, source: usize) -> HashMap<usize, u64> {
        let mut dist = HashMap::new();
        let mut heap = BinaryHeap::new();
    
        dist.insert(source, 0);
        heap.push(State { cost: 0, node: source });
    
        while let Some(State { cost, node }) = heap.pop() {
            if cost > *dist.get(&node).unwrap_or(&u64::MAX) { continue; }
    
            if let Some(edges) = graph.get(&node) {
                for edge in edges {
                    let new_dist = cost + edge.weight;
                    if new_dist < *dist.get(&edge.to).unwrap_or(&u64::MAX) {
                        dist.insert(edge.to, new_dist);
                        heap.push(State { cost: new_dist, node: edge.to });
                    }
                }
            }
        }
        dist
    }

    Key Differences

  • Mutability: OCaml's IntMap is truly immutable (new map per update). Rust's HashMap is mutated in place but owned — Rust's type system prevents the bugs that mutation usually causes.
  • Priority Queue: OCaml builds a functional sorted list (O(n) insert). Rust uses BinaryHeap (O(log n) insert/pop) but needs a wrapper type with reversed Ord since Rust only provides max-heap.
  • Type Safety: Both languages prevent type errors, but Rust additionally prevents iterator invalidation and use-after-free — critical in graph algorithms where nodes reference other nodes.
  • Performance: Rust's version is closer to C speed (no GC, no boxing of integers). OCaml's purely functional version allocates more but is elegant and correct by construction.
  • OCaml Approach

    OCaml uses a purely functional priority queue (sorted association list) and immutable IntMap for distances. The algorithm is expressed as a recursive loop with pattern matching:

    let rec loop pq dist =
      match PQ.pop pq with
      | None -> dist
      | Some ((d, u), pq') ->
        (* fold over neighbors, accumulate new distances *)
    

    Key features:

  • No mutation — distance map is rebuilt at each step via IntMap.add
  • Functional PQ — insertion maintains sorted order, pop returns head
  • Pattern matching — clean None/Some dispatch replaces while loops
  • Full Source

    #![allow(clippy::all)]
    //! Dijkstra's Shortest Path — Functional Rust with BinaryHeap and immutable-style updates.
    //!
    //! Demonstrates how Rust's ownership model naturally prevents the aliasing bugs
    //! that plague mutable graph algorithms in imperative languages.
    
    use std::cmp::Ordering;
    use std::collections::{BinaryHeap, HashMap};
    
    /// A weighted edge in the graph.
    #[derive(Clone, Debug)]
    pub struct Edge {
        pub to: usize,
        pub weight: u64,
    }
    
    /// Wrapper for BinaryHeap to get min-heap behavior (Rust's BinaryHeap is max-heap).
    #[derive(Clone, Eq, PartialEq)]
    struct State {
        cost: u64,
        node: usize,
    }
    
    impl Ord for State {
        fn cmp(&self, other: &Self) -> Ordering {
            // Reverse ordering for min-heap
            other
                .cost
                .cmp(&self.cost)
                .then_with(|| self.node.cmp(&other.node))
        }
    }
    
    impl PartialOrd for State {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            Some(self.cmp(other))
        }
    }
    
    /// Adjacency list representation of a weighted directed graph.
    pub type Graph = HashMap<usize, Vec<Edge>>;
    
    /// Build a graph from a list of (from, to, weight) tuples.
    /// Functional style: fold over edges to construct the adjacency map.
    pub fn build_graph(edges: &[(usize, usize, u64)]) -> Graph {
        edges
            .iter()
            .fold(HashMap::new(), |mut graph, &(from, to, weight)| {
                graph.entry(from).or_default().push(Edge { to, weight });
                graph
            })
    }
    
    /// Compute shortest distances from `source` to all reachable nodes.
    ///
    /// Returns a HashMap<usize, u64> mapping each node to its shortest distance.
    /// Unreachable nodes are absent from the result.
    ///
    /// # Functional patterns used:
    /// - **Fold-like iteration** over neighbors (functional accumulation)
    /// - **Immutable result map** built incrementally
    /// - **Pattern matching** on heap state
    pub fn dijkstra(graph: &Graph, source: usize) -> HashMap<usize, u64> {
        let mut dist: HashMap<usize, u64> = HashMap::new();
        let mut heap = BinaryHeap::new();
    
        dist.insert(source, 0);
        heap.push(State {
            cost: 0,
            node: source,
        });
    
        while let Some(State { cost, node }) = heap.pop() {
            // Skip stale entries — functional equivalent of "already visited"
            if cost > *dist.get(&node).unwrap_or(&u64::MAX) {
                continue;
            }
    
            // Fold over neighbors: accumulate new distances
            if let Some(edges) = graph.get(&node) {
                for edge in edges {
                    let new_dist = cost + edge.weight;
                    let current = *dist.get(&edge.to).unwrap_or(&u64::MAX);
    
                    if new_dist < current {
                        dist.insert(edge.to, new_dist);
                        heap.push(State {
                            cost: new_dist,
                            node: edge.to,
                        });
                    }
                }
            }
        }
    
        dist
    }
    
    /// Reconstruct the shortest path from source to target.
    /// Uses functional backtracking through the distance map.
    pub fn shortest_path(graph: &Graph, source: usize, target: usize) -> Option<(u64, Vec<usize>)> {
        let dist = dijkstra(graph, source);
        let total_dist = *dist.get(&target)?;
    
        // Backtrack from target to source
        let mut path = vec![target];
        let mut current = target;
    
        while current != source {
            let prev = graph
                .iter()
                .flat_map(|(&from, edges)| {
                    edges
                        .iter()
                        .filter(move |e| e.to == current)
                        .map(move |e| (from, e.weight))
                })
                .filter(|&(from, weight)| {
                    dist.get(&from)
                        .is_some_and(|&d| d + weight == *dist.get(&current).unwrap())
                })
                .map(|(from, _)| from)
                .next()?;
    
            path.push(prev);
            current = prev;
        }
    
        path.reverse();
        Some((total_dist, path))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        fn sample_graph() -> Graph {
            //  0 --1-- 1 --2-- 2
            //  |              |
            //  4              1
            //  |              |
            //  3 ------3----- 4
            build_graph(&[(0, 1, 1), (1, 2, 2), (0, 3, 4), (2, 4, 1), (3, 4, 3)])
        }
    
        #[test]
        fn test_source_distance_is_zero() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&0], 0);
        }
    
        #[test]
        fn test_direct_neighbor() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&1], 1);
        }
    
        #[test]
        fn test_two_hops() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&2], 3); // 0->1 (1) + 1->2 (2) = 3
        }
    
        #[test]
        fn test_shortest_via_longer_path() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&3], 4); // direct 0->3 = 4
        }
    
        #[test]
        fn test_shortest_to_distant_node() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&4], 4); // 0->1->2->4 = 1+2+1 = 4 (shorter than 0->3->4 = 4+3 = 7)
        }
    
        #[test]
        fn test_unreachable_node() {
            let g = build_graph(&[(0, 1, 1)]);
            let dist = dijkstra(&g, 0);
            assert!(!dist.contains_key(&99));
        }
    
        #[test]
        fn test_single_node_graph() {
            let g = build_graph(&[]);
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&0], 0);
            assert_eq!(dist.len(), 1);
        }
    
        #[test]
        fn test_shortest_path_reconstruction() {
            let g = sample_graph();
            let (cost, path) = shortest_path(&g, 0, 4).unwrap();
            assert_eq!(cost, 4);
            assert_eq!(path, vec![0, 1, 2, 4]);
        }
    
        #[test]
        fn test_path_to_self() {
            let g = sample_graph();
            let (cost, path) = shortest_path(&g, 0, 0).unwrap();
            assert_eq!(cost, 0);
            assert_eq!(path, vec![0]);
        }
    
        #[test]
        fn test_empty_graph_isolation() {
            let g = build_graph(&[]);
            let result = shortest_path(&g, 0, 5);
            assert!(result.is_none());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        fn sample_graph() -> Graph {
            //  0 --1-- 1 --2-- 2
            //  |              |
            //  4              1
            //  |              |
            //  3 ------3----- 4
            build_graph(&[(0, 1, 1), (1, 2, 2), (0, 3, 4), (2, 4, 1), (3, 4, 3)])
        }
    
        #[test]
        fn test_source_distance_is_zero() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&0], 0);
        }
    
        #[test]
        fn test_direct_neighbor() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&1], 1);
        }
    
        #[test]
        fn test_two_hops() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&2], 3); // 0->1 (1) + 1->2 (2) = 3
        }
    
        #[test]
        fn test_shortest_via_longer_path() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&3], 4); // direct 0->3 = 4
        }
    
        #[test]
        fn test_shortest_to_distant_node() {
            let g = sample_graph();
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&4], 4); // 0->1->2->4 = 1+2+1 = 4 (shorter than 0->3->4 = 4+3 = 7)
        }
    
        #[test]
        fn test_unreachable_node() {
            let g = build_graph(&[(0, 1, 1)]);
            let dist = dijkstra(&g, 0);
            assert!(!dist.contains_key(&99));
        }
    
        #[test]
        fn test_single_node_graph() {
            let g = build_graph(&[]);
            let dist = dijkstra(&g, 0);
            assert_eq!(dist[&0], 0);
            assert_eq!(dist.len(), 1);
        }
    
        #[test]
        fn test_shortest_path_reconstruction() {
            let g = sample_graph();
            let (cost, path) = shortest_path(&g, 0, 4).unwrap();
            assert_eq!(cost, 4);
            assert_eq!(path, vec![0, 1, 2, 4]);
        }
    
        #[test]
        fn test_path_to_self() {
            let g = sample_graph();
            let (cost, path) = shortest_path(&g, 0, 0).unwrap();
            assert_eq!(cost, 0);
            assert_eq!(path, vec![0]);
        }
    
        #[test]
        fn test_empty_graph_isolation() {
            let g = build_graph(&[]);
            let result = shortest_path(&g, 0, 5);
            assert!(result.is_none());
        }
    }

    Deep Comparison

    OCaml vs Rust: Dijkstra's Shortest Path

    Side-by-Side Code

    OCaml

    (* Priority queue as sorted association list — purely functional *)
    module PQ = struct
      type t = (int * int) list
    
      let insert dist node pq =
        let rec go = function
          | [] -> [(dist, node)]
          | ((d, _) as x) :: rest ->
            if dist <= d then (dist, node) :: x :: rest
            else x :: go rest
        in go pq
    
      let pop = function
        | [] -> None
        | (d, n) :: rest -> Some ((d, n), rest)
    end
    
    let dijkstra graph source =
      let dist = IntMap.singleton source 0 in
      let pq = PQ.insert 0 source PQ.empty in
      let rec loop pq dist =
        match PQ.pop pq with
        | None -> dist
        | Some ((d, u), pq') ->
          let current_dist = try IntMap.find u dist with Not_found -> max_int in
          if d > current_dist then loop pq' dist
          else
            let neighbors = try IntMap.find u graph with Not_found -> [] in
            let pq'', dist' =
              List.fold_left (fun (pq_acc, dist_acc) (v, w) ->
                let new_dist = d + w in
                let old_dist = try IntMap.find v dist_acc with Not_found -> max_int in
                if new_dist < old_dist then
                  (PQ.insert new_dist v pq_acc, IntMap.add v new_dist dist_acc)
                else (pq_acc, dist_acc)
              ) (pq', dist) neighbors
            in loop pq'' dist'
      in loop pq dist
    

    Rust (idiomatic — BinaryHeap min-heap via reversed Ord)

    #[derive(Clone, Eq, PartialEq)]
    struct State { cost: u64, node: usize }
    
    impl Ord for State {
        fn cmp(&self, other: &Self) -> Ordering {
            // Reverse ordering: BinaryHeap is max-heap, we want min-heap
            other.cost.cmp(&self.cost).then_with(|| self.node.cmp(&other.node))
        }
    }
    impl PartialOrd for State {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
    }
    
    pub fn dijkstra(graph: &Graph, source: usize) -> HashMap<usize, u64> {
        let mut dist = HashMap::new();
        let mut heap = BinaryHeap::new();
    
        dist.insert(source, 0);
        heap.push(State { cost: 0, node: source });
    
        while let Some(State { cost, node }) = heap.pop() {
            if cost > *dist.get(&node).unwrap_or(&u64::MAX) { continue; }
    
            if let Some(edges) = graph.get(&node) {
                for edge in edges {
                    let new_dist = cost + edge.weight;
                    if new_dist < *dist.get(&edge.to).unwrap_or(&u64::MAX) {
                        dist.insert(edge.to, new_dist);
                        heap.push(State { cost: new_dist, node: edge.to });
                    }
                }
            }
        }
        dist
    }
    

    Rust (functional graph construction — fold over edge list)

    pub fn build_graph(edges: &[(usize, usize, u64)]) -> Graph {
        edges.iter().fold(HashMap::new(), |mut graph, &(from, to, weight)| {
            graph.entry(from).or_default().push(Edge { to, weight });
            graph
        })
    }
    

    Type Signatures

    ConceptOCamlRust
    Distance map typeint IntMap.tHashMap<usize, u64>
    Priority queue(int * int) list (sorted)BinaryHeap<State>
    Graph type(int * int) list IntMap.tHashMap<usize, Vec<Edge>>
    Dijkstra signaturegraph -> int -> int IntMap.tfn dijkstra(graph: &Graph, source: usize) -> HashMap<usize, u64>
    Optional result'a optionOption<T>
    Fold accumulator('a * 'b) tuple(pq_acc, dist_acc) destructured

    Key Insights

  • Priority Queue Strategy: OCaml builds a purely functional sorted list — insertion is O(n) but immutable. Rust uses BinaryHeap (O(log n)), but since Rust only provides max-heap, a custom Ord implementation reverses the comparison to achieve min-heap behavior. This is a classic Rust idiom: newtype + reversed Ord.
  • Immutability vs Ownership: OCaml's IntMap.add produces a new map each step — true immutability via structural sharing. Rust's HashMap is mutated in-place, but Rust's ownership model ensures no aliased mutable references exist, providing the same safety guarantees without the allocation cost.
  • Stale-Entry Handling: Both languages use the same logical pattern — push multiple entries for a node and skip outdated ones. OCaml checks d > current_dist via pattern match; Rust checks cost > dist[node] in the while let loop. The functional insight: lazy deletion is valid because we only commit a distance when it's minimal.
  • **fold_left vs Iterator Chains:** OCaml's List.fold_left explicitly threads (pq_acc, dist_acc) as a tuple through each neighbor. Rust expresses the same accumulation as a for loop over edges mutating dist and heap — idiomatic Rust prefers direct mutation of owned data over tuple threading when the data is already exclusively owned.
  • Path Reconstruction: Not present in the OCaml version (distance-only). Rust adds shortest_path using an iterator chain with .flat_map, .filter, .map, .next() to backtrack through the distance map — demonstrating how Rust iterator combinators replace OCaml's recursive list comprehensions for search problems.
  • When to Use Each Style

    Use idiomatic Rust (BinaryHeap + HashMap): When performance matters — this is O((V + E) log V) with low constant factors, no GC pauses, and no allocations per map update.

    Use the OCaml purely functional style: When you want proof-friendly code or need to checkpoint algorithm state (the immutable map is a snapshot of distances at each step, enabling backtracking or parallel exploration without copying).

    Exercises

  • Add bidirectional edges and verify shortest paths update correctly
  • Implement A* search by adding a heuristic function parameter
  • Build a functional version in Rust using im::HashMap (persistent data structure crate)
  • Add negative edge detection and return an error if found (Dijkstra doesn't handle negative weights)
  • Open Source Repos