ℹ️ Select 'Choose Exercise', or randomize 'Next Random Exercise' in selected language.

Choose Exercise:
Timer 00:00
WPM --
Score --
Acc --
Correct chars --

OCaml Immutable Graph: Breadth-First Search for Shortest Path (Unweighted)

OCaml

Goal -- WPM

Ready
Exercise Algorithm Area
1(* Immutable graph BFS for shortest path in an unweighted graph.
2Graph represented as an adjacency list: Map from node ID to list of neighbor IDs.
3Finds the shortest path (number of edges) from a source to a target node.
4*)
5
6module Graph =
7struct
8type node_id = int
9type graph = (node_id, node_id list) Hashtbl.t
10
11(* Helper function to add an edge to the graph *)
12let add_edge (graph : graph) (u : node_id) (v : node_id) : unit =
13let neighbors = try Hashtbl.find graph u with Not_found -> [] in
14Hashtbl.replace graph u (v :: neighbors)
15
16(* Helper function to create a graph from edges *)
17let create_graph (edges : (node_id * node_id) list) : graph =
18let graph = Hashtbl.create 10 in
19List.iter (fun (u, v) -> add_edge graph u v) edges;
20(* Ensure all nodes exist, even if they have no outgoing edges *)
21let all_nodes = List.fold_left (fun acc (u, v) -> u :: v :: acc) [] edges in
22List.sort_uniq compare all_nodes |> List.iter (fun node -> Hashtbl.add_if_absent graph node []) ;
23graph
24
25(* BFS implementation to find the shortest path *)
26let shortest_path (graph : graph) (source : node_id) (target : node_id) : node_id list option =
27(* Queue for BFS: stores nodes to visit *)
28let queue = ref ([] : node_id list) in
29(* Distances from source: map node_id to distance *)
30let distances = Hashtbl.create (Hashtbl.length graph) in
31(* Predecessors for path reconstruction: map node_id to its predecessor *)
32let predecessors = Hashtbl.create (Hashtbl.length graph) in
33
34(* Initialize distances and queue *)
35Hashtbl.iter (fun node _ ->
36Hashtbl.add distances node (-1) (* -1 indicates unreachable *)
37) graph;
38
39(* Handle edge case: source or target not in graph *)
40if not (Hashtbl.mem graph source) || not (Hashtbl.mem graph target) then
41None
42else if source = target then
43Some [source] (* Path to itself is just the node *)
44else
45begin
46Hashtbl.replace distances source 0;
47queue := source :: !queue;
48
49(* BFS loop *)
50while List.length !queue > 0 do
51let current_node = List.hd !queue in
52queue := List.tl !queue;
53
54let current_dist = Hashtbl.find distances current_node in
55let neighbors = try Hashtbl.find graph current_node with Not_found -> [] in
56
57List.iter (fun neighbor ->
58(* If neighbor has not been visited yet (distance is -1) *)
59if Hashtbl.find distances neighbor = -1 then
60begin
61Hashtbl.replace distances neighbor (current_dist + 1);
62Hashtbl.replace predecessors neighbor current_node;
63queue := neighbor :: !queue;
64end
65) neighbors
66done;
67
68(* Path reconstruction *)
69if Hashtbl.find distances target = -1 then
70None (* Target is unreachable *)
71else
72let rec reconstruct_path node path =
73match Hashtbl.find_opt predecessors node with
74| Some pred -> reconstruct_path pred (node :: path)
75| None -> node :: path (* Base case: reached source *)
76in
77Some (reconstruct_path target [])
78end
79end
80
81(* Example usage:
82let edges = [(0, 1); (0, 2); (1, 3); (2, 3); (3, 4)]
83let graph = Graph.create_graph edges
84let path = Graph.shortest_path graph 0 4
85(* Expected: Some [0; 1; 3; 4] or Some [0; 2; 3; 4] *)
86*)
Algorithm description viewbox

OCaml Immutable Graph: Breadth-First Search for Shortest Path (Unweighted)

Algorithm description:

This OCaml code implements Breadth-First Search (BFS) to find the shortest path in an unweighted directed graph. The graph is represented using an adjacency list. BFS explores the graph level by level, guaranteeing that the first time the target node is reached, it's via the shortest path. The algorithm uses a queue to manage nodes to visit and two hash tables to store distances from the source and predecessors for path reconstruction. This is a fundamental algorithm for network routing, finding the minimum number of steps between two points, and in many graph-based puzzles.

Algorithm explanation:

The `shortest_path` function initializes a queue, a `distances` map (all nodes to -1, source to 0), and a `predecessors` map. The BFS loop dequeues a node, explores its unvisited neighbors, updates their distances (current_dist + 1), records their predecessor, and enqueues them. This process continues until the queue is empty. If the target's distance is still -1, it's unreachable. Otherwise, the path is reconstructed by backtracking from the target using the `predecessors` map. The time complexity is O(V + E), where V is the number of vertices and E is the number of edges, as each vertex and edge is processed at most once. The space complexity is O(V) for the queue, distances, and predecessors. Edge cases include the source and target being the same, the target being unreachable, and the source or target not existing in the graph.

Pseudocode:

function shortest_path(graph, source, target):
  queue = empty queue
  distances = map of node -> distance (initialize all to -1)
  predecessors = map of node -> predecessor

  if source not in graph or target not in graph:
    return None
  if source == target:
    return [source]

  distances[source] = 0
  enqueue(queue, source)

  while queue is not empty:
    current_node = dequeue(queue)
    current_dist = distances[current_node]
    for each neighbor of current_node:
      if distances[neighbor] == -1:
        distances[neighbor] = current_dist + 1
        predecessors[neighbor] = current_node
        enqueue(queue, neighbor)

  if distances[target] == -1:
    return None (target unreachable)
  else:
    path = empty list
    current = target
    while current is not None:
      prepend current to path
      current = predecessors[current]
    return path