yog/pathfinding
Types
Result type for Bellman-Ford algorithm.
pub type BellmanFordResult(e) {
ShortestPath(path: Path(e))
NegativeCycle
NoPath
}
Constructors
-
ShortestPath(path: Path(e))A shortest path was found successfully
-
NegativeCycleA negative cycle was detected (reachable from source)
-
NoPathNo path exists from start to goal
Values
pub fn a_star(
in graph: model.Graph(n, e),
from start: Int,
to goal: Int,
with_zero zero: e,
with_add add: fn(e, e) -> e,
with_compare compare: fn(e, e) -> order.Order,
heuristic h: fn(Int, Int) -> e,
) -> option.Option(Path(e))
Finds the shortest path using A* search with a heuristic function.
A* is more efficient than Dijkstra when you have a good heuristic estimate of the remaining distance to the goal. The heuristic must be admissible (never overestimate the actual distance) to guarantee finding the shortest path.
Time Complexity: O((V + E) log V), but often faster than Dijkstra in practice
Parameters
heuristic: A function that estimates distance from any node to the goal. Must be admissible (h(n) ≤ actual distance) to guarantee shortest path.
Example
// Manhattan distance heuristic for grid
let h = fn(node, goal) {
int.absolute_value(node.x - goal.x) + int.absolute_value(node.y - goal.y)
}
pathfinding.a_star(
in: graph,
from: start,
to: goal,
with_zero: 0,
with_add: int.add,
with_compare: int.compare,
heuristic: h
)
pub fn bellman_ford(
in graph: model.Graph(n, e),
from start: Int,
to goal: Int,
with_zero zero: e,
with_add add: fn(e, e) -> e,
with_compare compare: fn(e, e) -> order.Order,
) -> BellmanFordResult(e)
Finds shortest path with support for negative edge weights using Bellman-Ford.
Unlike Dijkstra and A*, this algorithm can handle negative edge weights. It also detects negative cycles reachable from the source node.
Time Complexity: O(VE) where V is vertices and E is edges
Returns
ShortestPath(path): If a valid shortest path existsNegativeCycle: If a negative cycle is reachable from the start nodeNoPath: If no path exists from start to goal
Example
pathfinding.bellman_ford(
in: graph,
from: 1,
to: 5,
with_zero: 0,
with_add: int.add,
with_compare: int.compare
)
// => ShortestPath(Path([1, 3, 5], -2)) // Can have negative total weight
// or NegativeCycle // If cycle detected
// or NoPath // If unreachable
pub fn floyd_warshall(
in graph: model.Graph(n, e),
with_zero zero: e,
with_add add: fn(e, e) -> e,
with_compare compare: fn(e, e) -> order.Order,
) -> Result(dict.Dict(#(Int, Int), e), Nil)
Computes shortest paths between all pairs of nodes using the Floyd-Warshall algorithm.
Returns a nested dictionary where distances[i][j] gives the shortest distance from node i to node j.
If no path exists between two nodes, the pair will not be present in the dictionary.
Returns Error(Nil) if a negative cycle is detected in the graph.
Time Complexity: O(V³) Space Complexity: O(V²)
Parameters
zero: The identity element for addition (e.g.,0for integers,0.0for floats)add: Function to add two weightscompare: Function to compare two weights
Example
import gleam/dict
import gleam/int
import gleam/io
import yog
import yog/pathfinding
pub fn main() {
let graph =
yog.directed()
|> yog.add_node(1, "A")
|> yog.add_node(2, "B")
|> yog.add_node(3, "C")
|> yog.add_edge(from: 1, to: 2, with: 4)
|> yog.add_edge(from: 2, to: 3, with: 3)
|> yog.add_edge(from: 1, to: 3, with: 10)
case pathfinding.floyd_warshall(
in: graph,
with_zero: 0,
with_add: int.add,
with_compare: int.compare
) {
Ok(distances) -> {
// Query distance from node 1 to node 3
let assert Ok(row) = dict.get(distances, 1)
let assert Ok(dist) = dict.get(row, 3)
// dist = 7 (via node 2: 4 + 3)
io.println("Distance from 1 to 3: " <> int.to_string(dist))
}
Error(Nil) -> io.println("Negative cycle detected!")
}
}
Handling Negative Weights
Floyd-Warshall can handle negative edge weights and will detect negative cycles:
let graph_with_negative_cycle =
yog.directed()
|> yog.add_node(1, "A")
|> yog.add_node(2, "B")
|> yog.add_edge(from: 1, to: 2, with: 5)
|> yog.add_edge(from: 2, to: 1, with: -10)
case pathfinding.floyd_warshall(
in: graph_with_negative_cycle,
with_zero: 0,
with_add: int.add,
with_compare: int.compare
) {
Ok(_) -> io.println("No negative cycle")
Error(Nil) -> io.println("Negative cycle detected!") // This will execute
}
Use Cases
- Computing distance matrices for all node pairs
- Finding transitive closure of a graph
- Detecting negative cycles
- Preprocessing for queries about arbitrary node pairs
- Graph metrics (diameter, centrality)
pub fn shortest_path(
in graph: model.Graph(n, e),
from start: Int,
to goal: Int,
with_zero zero: e,
with_add add: fn(e, e) -> e,
with_compare compare: fn(e, e) -> order.Order,
) -> option.Option(Path(e))
Finds the shortest path between two nodes using Dijkstra’s algorithm.
Works with non-negative edge weights only. For negative weights, use bellman_ford.
Time Complexity: O((V + E) log V) with heap
Parameters
zero: The identity element for addition (e.g., 0 for integers)add: Function to add two weightscompare: Function to compare two weights
Example
pathfinding.shortest_path(
in: graph,
from: 1,
to: 5,
with_zero: 0,
with_add: int.add,
with_compare: int.compare
)
// => Some(Path([1, 2, 5], 15))
pub fn single_source_distances(
in graph: model.Graph(n, e),
from source: Int,
with_zero zero: e,
with_add add: fn(e, e) -> e,
with_compare compare: fn(e, e) -> order.Order,
) -> dict.Dict(Int, e)
Computes shortest distances from a source node to all reachable nodes.
Returns a dictionary mapping each reachable node to its shortest distance from the source. Unreachable nodes are not included in the result.
This is useful when you need distances to multiple destinations, or want
to find the closest target among many options. More efficient than running
shortest_path multiple times.
Time Complexity: O((V + E) log V) with heap
Parameters
zero: The identity element for addition (e.g., 0 for integers)add: Function to add two weightscompare: Function to compare two weights
Example
// Find distances from node 1 to all reachable nodes
let distances = pathfinding.single_source_distances(
in: graph,
from: 1,
with_zero: 0,
with_add: int.add,
with_compare: int.compare
)
// => dict.from_list([#(1, 0), #(2, 5), #(3, 8), #(4, 15)])
// Find closest target among many options
let targets = [10, 20, 30]
let closest = targets
|> list.filter_map(fn(t) { dict.get(distances, t) })
|> list.sort(int.compare)
|> list.first
Use Cases
- Finding nearest target among multiple options
- Computing distance maps for game AI
- Network routing table generation
- Graph analysis (centrality measures)
- Reverse pathfinding (with
transform.transpose)