Current section
Files
Jump to
Current section
Files
lib/yog/pathfinding/bellman_ford.ex
defmodule Yog.Pathfinding.BellmanFord do
@moduledoc """
Bellman-Ford algorithm for single-source shortest paths with negative weight support.
Bellman-Ford can handle graphs with negative edge weights and detects negative
cycles. It's slower than Dijkstra but more versatile.
## Algorithm Characteristics
- **Time Complexity**: O(V × E)
- **Space Complexity**: O(V)
- **Requirements**: No negative cycles (detects them)
- **Optimality**: Optimal for graphs without negative cycles
## When to Use
- When edge weights may be negative
- When you need to detect negative cycles
- For all-pairs shortest paths (via V runs)
- When Dijkstra's non-negative weight requirement can't be met
## Negative Cycles
A negative cycle is a cycle whose total weight is negative. If one is reachable
from the source, shortest paths are undefined (you can loop forever to decrease
cost). Bellman-Ford detects this case.
## Examples
# Graph with negative weights but no negative cycles
graph = Yog.new()
|> Yog.add_edge(:a, :b, 4)
|> Yog.add_edge(:b, :c, -3)
|> Yog.add_edge(:a, :c, 2)
compare = fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
BellmanFord.bellman_ford(graph, :a, :c, 0, &(&1+&2), compare)
#=> {:shortest_path, {:path, [:a, :b, :c], 1}}
"""
alias Yog.Pathfinding.Utils
@typedoc "Result type for Bellman-Ford shortest path queries"
@type result(weight) ::
{:shortest_path, Utils.path(weight)} | :negative_cycle | :no_path
@typedoc "Result type for implicit Bellman-Ford queries"
@type implicit_result(weight) ::
{:found_goal, weight} | :detected_negative_cycle | :no_goal
# ============================================================
# Keyword-style API (for Pathfinding module delegation)
# ============================================================
@doc """
Find shortest path using Bellman-Ford with keyword options.
## Options
* `:in` - The graph to search
* `:from` - Starting node
* `:to` - Target node
* `:zero` - Identity value for the weight type
* `:add` - Function to add two weights
* `:compare` - Function to compare weights (`:lt`, `:eq`, `:gt`)
## Examples
Pathfinding.bellman_ford(
in: graph,
from: :a,
to: :c,
zero: 0,
add: &(&1 + &2),
compare: fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
)
"""
@spec bellman_ford(keyword()) :: result(any())
def bellman_ford(opts) do
graph = Keyword.fetch!(opts, :in)
from = Keyword.fetch!(opts, :from)
to = Keyword.fetch!(opts, :to)
zero = Keyword.fetch!(opts, :zero)
add = Keyword.fetch!(opts, :add)
compare = Keyword.fetch!(opts, :compare)
bellman_ford(graph, from, to, zero, add, compare)
end
@doc """
Implicit Bellman-Ford using keyword options.
## Options
* `:from` - Starting state
* `:successors_with_cost` - Function returning neighbors with costs
* `:is_goal` - Function to check if a state is the goal
* `:zero` - Identity value for the weight type
* `:add` - Function to add two weights
* `:compare` - Function to compare weights
## Examples
Pathfinding.implicit_bellman_ford(
from: 1,
successors_with_cost: fn n -> [{n+1, -1}] end,
is_goal: fn n -> n == 10 end,
zero: 0,
add: &(&1 + &2),
compare: fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
)
"""
@spec implicit_bellman_ford(keyword()) :: implicit_result(any())
def implicit_bellman_ford(opts) do
from = Keyword.fetch!(opts, :from)
successors = Keyword.fetch!(opts, :successors_with_cost)
is_goal = Keyword.fetch!(opts, :is_goal)
zero = Keyword.fetch!(opts, :zero)
add = Keyword.fetch!(opts, :add)
compare = Keyword.fetch!(opts, :compare)
implicit_bellman_ford(from, successors, is_goal, zero, add, compare)
end
@doc """
Implicit Bellman-Ford with key function using keyword options.
## Options
* `:from` - Starting state
* `:successors_with_cost` - Function returning neighbors with costs
* `:visited_by` - Function to extract a key for visited tracking
* `:is_goal` - Function to check if a state is the goal
* `:zero` - Identity value for the weight type
* `:add` - Function to add two weights
* `:compare` - Function to compare weights
"""
@spec implicit_bellman_ford_by(keyword()) :: implicit_result(any())
def implicit_bellman_ford_by(opts) do
from = Keyword.fetch!(opts, :from)
successors = Keyword.fetch!(opts, :successors_with_cost)
visited_by = Keyword.fetch!(opts, :visited_by)
is_goal = Keyword.fetch!(opts, :is_goal)
zero = Keyword.fetch!(opts, :zero)
add = Keyword.fetch!(opts, :add)
compare = Keyword.fetch!(opts, :compare)
implicit_bellman_ford_by(from, successors, visited_by, is_goal, zero, add, compare)
end
# ============================================================
# Direct API
# ============================================================
@doc """
Find the shortest path between two nodes using Bellman-Ford.
## Parameters
* `graph` - The graph to search
* `from` - Starting node
* `to` - Target node
* `zero` - Identity value for the weight type
* `add` - Function to add two weights
* `compare` - Function to compare weights (`:lt`, `:eq`, `:gt`)
## Returns
* `{:shortest_path, path}` - A `Path` struct containing the nodes and weight
* `:negative_cycle` - A negative cycle was detected
* `:no_path` - No path exists between the nodes
## Examples
iex> graph = Yog.directed()
...> |> Yog.add_node(:a, nil)
...> |> Yog.add_node(:b, nil)
...> |> Yog.add_node(:c, nil)
...> |> Yog.add_edge!(:a, :b, 4)
...> |> Yog.add_edge!(:b, :c, -3)
iex> compare = fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
iex> BellmanFord.bellman_ford(graph, :a, :c, 0, &(&1 + &2), compare)
{:shortest_path, {:path, [:a, :b, :c], 1}}
iex> # Graph with negative cycle
iex> bad_graph = Yog.directed()
...> |> Yog.add_node(:a, nil)
...> |> Yog.add_node(:b, nil)
...> |> Yog.add_edge!(:a, :b, 1)
...> |> Yog.add_edge!(:b, :a, -3)
iex> compare = fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
iex> BellmanFord.bellman_ford(bad_graph, :a, :b, 0, &(&1 + &2), compare)
:negative_cycle
"""
@spec bellman_ford(
Yog.t(),
Yog.node_id(),
Yog.node_id(),
weight,
(weight, weight -> weight),
(weight, weight -> :lt | :eq | :gt)
) :: result(weight)
when weight: var
def bellman_ford(graph, from, to, zero, add, compare) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
case :yog@pathfinding@bellman_ford.bellman_ford(graph, from, to, zero, add, gleam_compare) do
{:shortest_path, {:path, nodes, weight}} ->
{:shortest_path, Utils.path(nodes, weight)}
:negative_cycle ->
:negative_cycle
:no_path ->
:no_path
end
end
@doc """
Find shortest path using Bellman-Ford with integer weights.
## Examples
iex> graph = Yog.directed()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge!(1, 2, 4)
...> |> Yog.add_edge!(2, 3, -3)
iex> BellmanFord.bellman_ford_int(graph, 1, 3)
{:shortest_path, {:path, [1, 2, 3], 1}}
"""
@spec bellman_ford_int(Yog.t(), Yog.node_id(), Yog.node_id()) :: result(integer())
def bellman_ford_int(graph, from, to) do
:yog@pathfinding@bellman_ford.bellman_ford_int(graph, from, to)
|> wrap_result()
end
@doc """
Find shortest path using Bellman-Ford with float weights.
Convenience function for float weights.
"""
@spec bellman_ford_float(Yog.t(), Yog.node_id(), Yog.node_id()) :: result(float())
def bellman_ford_float(graph, from, to) do
:yog@pathfinding@bellman_ford.bellman_ford_float(graph, from, to)
|> wrap_result()
end
@doc """
Computes relaxation passes for all-pairs shortest paths.
Returns a map of all node distances after V-1 relaxation passes.
"""
@spec relaxation_passes(
Yog.graph(),
Yog.node_id(),
any(),
(any(), any() -> any()),
(any(), any() -> :lt | :eq | :gt)
) :: %{Yog.node_id() => any()}
def relaxation_passes(graph, from, zero, add, compare) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
all_nodes = :yog@model.all_nodes(graph)
node_count = length(all_nodes)
initial_distances = :gleam@dict.from_list([{from, zero}])
initial_predecessors = :gleam@dict.new()
{distances, _predecessors} =
:yog@pathfinding@bellman_ford.relaxation_passes(
graph,
all_nodes,
initial_distances,
initial_predecessors,
node_count - 1,
add,
gleam_compare
)
distances
|> :gleam@dict.to_list()
|> Map.new()
end
@doc """
Checks if the graph contains a negative cycle reachable from the source.
"""
@spec has_negative_cycle?(
Yog.graph(),
Yog.node_id(),
any(),
(any(), any() -> any()),
(any(), any() -> :lt | :eq | :gt)
) :: boolean()
def has_negative_cycle?(graph, from, zero, add, compare) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
:yog@pathfinding@bellman_ford.has_negative_cycle(graph, from, zero, add, gleam_compare)
end
@doc """
Reconstructs a path from the predecessor map returned by Bellman-Ford.
"""
@spec reconstruct_path(%{Yog.node_id() => Yog.node_id()}, Yog.node_id(), Yog.node_id(), any()) ::
Utils.path(any())
def reconstruct_path(predecessors, from, to, weight) do
# Convert Elixir map to Gleam dict
gleam_pred =
predecessors
|> Map.to_list()
|> :gleam@dict.from_list()
{:path, nodes, weight} =
:yog@pathfinding@bellman_ford.reconstruct_path(gleam_pred, from, to, weight)
Utils.path(nodes, weight)
end
@doc """
Run Bellman-Ford on an implicit (generated) graph.
Similar to implicit Dijkstra, but supports negative edge weights and can
detect negative cycles.
## Parameters
* `from` - Starting state
* `successors` - Function `state -> [{neighbor, cost}]`
* `is_goal` - Function `state -> boolean` to check if goal reached
* `zero` - Identity value for the weight type
* `add` - Function to add two weights
* `compare` - Function to compare weights
## Returns
* `{:found_goal, cost}` - Minimum cost to reach goal
* `:detected_negative_cycle` - A negative cycle was found
* `:no_goal` - Goal is unreachable
## Examples
# Search with negative weights
iex> successors = fn
...> 1 -> [{2, -1}]
...> 2 -> [{3, -2}]
...> 3 -> [{4, -3}]
...> 4 -> []
...> end
iex> BellmanFord.implicit_bellman_ford(
...> 1, successors, fn x -> x == 4 end,
...> 0, &(&1 + &2), fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
...> )
{:found_goal, -6}
"""
@spec implicit_bellman_ford(
state,
(state -> [{state, cost}]),
(state -> boolean),
cost,
(cost, cost -> cost),
(cost, cost -> :lt | :eq | :gt)
) :: implicit_result(cost)
when state: var, cost: var
def implicit_bellman_ford(from, successors, is_goal, zero, add, compare) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
:yog@pathfinding@bellman_ford.implicit_bellman_ford(
from,
successors,
is_goal,
zero,
add,
gleam_compare
)
end
@doc """
Implicit Bellman-Ford with a key function for visited state tracking.
Similar to `implicit_bellman_ford/6`, but uses a key function for visited tracking.
## Parameters
* `from` - Starting state
* `successors` - Function `state -> [{neighbor, cost}]`
* `key_fn` - Function `state -> key` for visited tracking
* `is_goal` - Function `state -> boolean` to check if goal reached
* `zero` - Identity value for the weight type
* `add` - Function to add two weights
* `compare` - Function to compare weights
"""
@spec implicit_bellman_ford_by(
state,
(state -> [{state, cost}]),
(state -> term()),
(state -> boolean),
cost,
(cost, cost -> cost),
(cost, cost -> :lt | :eq | :gt)
) :: implicit_result(cost)
when state: var, cost: var
def implicit_bellman_ford_by(from, successors, key_fn, is_goal, zero, add, compare) do
gleam_compare = fn a, b ->
case compare.(a, b) do
:lt -> :lt
:eq -> :eq
:gt -> :gt
end
end
:yog@pathfinding@bellman_ford.implicit_bellman_ford_by(
from,
successors,
key_fn,
is_goal,
zero,
add,
gleam_compare
)
end
# Private helper to wrap Gleam result
defp wrap_result({:shortest_path, {:path, nodes, weight}}) do
{:shortest_path, Utils.path(nodes, weight)}
end
defp wrap_result(:negative_cycle), do: :negative_cycle
defp wrap_result(:no_path), do: :no_path
end