Yog.Functional.Algorithms (YogEx v0.98.0)

Copy Markdown View Source

Classic graph algorithms using purely functional inductive decomposition.

Each algorithm leverages Yog.Functional.Model.match/2 as its core operation. By extracting a node and working with the resulting shrunken graph, these implementations avoid mutable state and explicit "visited" sets. Correctness and termination follow naturally from the inductive structure.

Available Algorithms

AlgorithmFunctionTime Complexity
Topological Sorttopsort/1O(V + E)
Dijkstra's Shortest Pathshortest_path/3O((V + E) log V)
Distancesdistances/2Compute all-node distances from source
Prim's MSTmst_prim/1O(E log V)
Kosaraju's SCCscc/1O(V + E)

Key Principle

Every algorithm consumes the graph structurally: match/2 removes a node and all its incident edges, so already-processed nodes simply won't be found in the remaining graph. No external "visited" bookkeeping is needed.

References

Summary

Functions

Computes the distance from a start node to all reachable nodes using Dijkstra's algorithm. Returns %{node_id => distance}.

Finds the Minimum Spanning Tree of the connected component containing the first node. Returns {:ok, [{from, to, weight}]} or {:ok, []} for empty graphs.

Finds the Strongly Connected Components (SCCs) of a directed graph. Returns a list of lists, where each inner list contains the node IDs of one SCC.

Finds the shortest path between two nodes using Dijkstra's algorithm. Returns {:ok, [node_ids], total_distance} or {:error, :no_path}.

Performs a Topological Sort by repeatedly extracting nodes with 0 in-degree. Returns {:ok, [node_ids]} or {:error, :cycle_detected}.

Functions

distances(graph, start)

Computes the distance from a start node to all reachable nodes using Dijkstra's algorithm. Returns %{node_id => distance}.

Examples

iex> alias Yog.Functional.{Model, Algorithms}
iex> graph = Model.empty() |> Model.put_node(1, "A") |> Model.put_node(2, "B")
...> |> Model.add_edge!(1, 2, 10)
iex> Algorithms.distances(graph, 1)
%{1 => 0, 2 => 10}

mst_prim(graph)

Finds the Minimum Spanning Tree of the connected component containing the first node. Returns {:ok, [{from, to, weight}]} or {:ok, []} for empty graphs.

Treats the graph as undirected by extracting both in and out edges from each context.

Inductive approach (Prim's): start from any node (extracted via match/2), insert its adjacent edges into a sorted priority queue, then repeatedly dequeue the minimum-weight edge whose target is still in the unvisited graph. match/2 is used to extract the target: if it's already been visited, match/2 returns {:error, :not_found} and we skip to the next candidate.

scc(graph)

Finds the Strongly Connected Components (SCCs) of a directed graph. Returns a list of lists, where each inner list contains the node IDs of one SCC.

Uses Kosaraju's two-pass algorithm, adapted for functional inductive graphs:

  1. Pass 1: compute the DFS finishing order of all nodes.
  2. Reverse the graph's edges.
  3. Pass 2: extract components by running DFS on the reversed graph in the compute order.

Due to the inductive match/2 operations, nodes visited in one component are naturally removed from the graph, preventing them from bleeding into subsequent components.

Examples

iex> alias Yog.Functional.{Model, Algorithms}
iex> graph = Model.empty() |> Model.put_node(1, "A") |> Model.put_node(2, "B")
...> |> Model.add_edge!(1, 2) |> Model.add_edge!(2, 1)
iex> sccs = Algorithms.scc(graph)
iex> Enum.map(sccs, &Enum.sort/1) |> Enum.sort()
[[1, 2]]

shortest_path(graph, start_id, target_id)

Finds the shortest path between two nodes using Dijkstra's algorithm. Returns {:ok, [node_ids], total_distance} or {:error, :no_path}.

Edge labels are used as weights (defaults to 1 if nil).

Inductive approach: we maintain a sorted priority queue of {dist, node, path} entries. Each step extracts the minimum-distance frontier node using match/2. Because match/2 removes the node from the graph, we naturally skip already-settled nodes — they simply won't be found anymore.

Examples

iex> alias Yog.Functional.{Model, Algorithms}
iex> graph = Model.empty() |> Model.put_node(1, "A") |> Model.put_node(2, "B")
...> |> Model.add_edge!(1, 2, 10)
iex> Algorithms.shortest_path(graph, 1, 2)
{:ok, [1, 2], 10}

topsort(graph)

Performs a Topological Sort by repeatedly extracting nodes with 0 in-degree. Returns {:ok, [node_ids]} or {:error, :cycle_detected}.

Inductive approach: each round, we scan for a node whose in_edges map is empty in the current (shrunken) graph, extract it with match/2, and recurse. When an edge is removed by match/2, affected neighbours' in_edges are automatically updated by the inductive model, so the in-degree invariant is always up to date without a separate tracking structure.

Examples

iex> alias Yog.Functional.{Model, Algorithms}
iex> graph = Model.empty() |> Model.put_node(1, "A") |> Model.put_node(2, "B")
...> |> Model.add_edge!(1, 2)
iex> {:ok, order} = Algorithms.topsort(graph)
iex> order
[1, 2]