Current section

Files

Jump to
yog_ex lib yog pathfinding floyd_warshall.ex
Raw

lib/yog/pathfinding/floyd_warshall.ex

defmodule Yog.Pathfinding.FloydWarshall do
@moduledoc """
[Floyd-Warshall algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm)
for all-pairs shortest paths in weighted graphs.
The Floyd-Warshall algorithm finds the shortest paths between all pairs of nodes
in a single execution. It uses dynamic programming to iteratively improve shortest
path estimates by considering each node as a potential intermediate vertex.
## Algorithm
| Algorithm | Function | Complexity | Best For |
|-----------|----------|------------|----------|
| [Floyd-Warshall](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) | `floyd_warshall/4` | O(V³) | Dense graphs, all-pairs paths |
## Key Concepts
- **Dynamic Programming**: Builds solution from smaller subproblems
- **K-Intermediate Nodes**: After k iterations, paths use only nodes {1,...,k} as intermediates
- **Path Reconstruction**: Predecessor matrix allows full path recovery
- **Transitive Closure**: Can be adapted for reachability (boolean weights)
## The DP Recurrence
```
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
```
For each intermediate node k, check if going through k improves the path from i to j.
## Comparison with Running Dijkstra V Times
| Approach | Complexity | Best For |
|----------|------------|----------|
| Floyd-Warshall | O(V³) | Dense graphs (E ≈ V²) |
| V × Dijkstra | O(V(V+E) log V) | Sparse graphs |
| Johnson's | O(V² log V + VE) | Sparse graphs with negative weights |
**Rule of thumb**: Use Floyd-Warshall when E > V × log V (fairly dense)
## Negative Cycles
The algorithm can detect negative cycles: after completion, if any node has
dist[node][node] < 0, a negative cycle exists.
## Variants
- **Transitive Closure**: Use boolean OR instead of min-plus (Warshall's algorithm)
- **Successor Matrix**: Track next hop for path reconstruction
## Use Cases
- **All-pairs routing**: Precompute distances for fast lookup
- **Transitive closure**: Reachability queries in databases
- **Centrality metrics**: Closeness and betweenness calculations
- **Graph analysis**: Detecting negative cycles
## History
Published independently by Robert Floyd (1962), Stephen Warshall (1962),
and Bernard Roy (1959). Floyd's version included path reconstruction.
## References
- [Wikipedia: Floyd-Warshall Algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm)
- [CP-Algorithms: Floyd-Warshall](https://cp-algorithms.com/graph/all-pair-shortest-path-floyd-warshall.html)
"""
@typedoc """
Distance matrix: map from `{from, to}` tuple to distance.
"""
@type distance_matrix :: %{{Yog.node_id(), Yog.node_id()} => any()}
@doc """
Computes shortest paths between all pairs of nodes using Floyd-Warshall.
**Time Complexity:** O(V³)
Returns `{:ok, distance_matrix}` on success, or `{:error, :negative_cycle}`
if a negative cycle is detected.
## Parameters
- `graph` - The graph to analyze
- `zero` - Identity element for addition
- `add` - Function to add two weights
- `compare` - Function to compare two weights
## Examples
# Triangle graph with all-pairs distances
iex> graph = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge!(from: 1, to: 2, with: 4)
...> |> Yog.add_edge!(from: 2, to: 3, with: 1)
...> |> Yog.add_edge!(from: 1, to: 3, with: 10)
iex> compare = fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
iex> {:ok, distances} = Yog.Pathfinding.FloydWarshall.floyd_warshall(graph, 0, &(&1 + &2), compare)
iex> # Shortest path from 1 to 3 should be 1->2->3 = 5, not direct 10
...> distances[{1, 3}]
5
# Negative cycle detection
iex> bad_graph = Yog.directed()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_edge!(from: 1, to: 2, with: 1)
...> |> Yog.add_edge!(from: 2, to: 1, with: -3)
iex> compare = fn a, b when a < b -> :lt; a, b when a > b -> :gt; _, _ -> :eq end
iex> Yog.Pathfinding.FloydWarshall.floyd_warshall(bad_graph, 0, &(&1 + &2), compare)
{:error, :negative_cycle}
"""
@spec floyd_warshall(
Yog.graph(),
any(),
(any(), any() -> any()),
(any(), any() -> :lt | :eq | :gt)
) :: {:ok, distance_matrix()} | {:error, :negative_cycle}
def floyd_warshall(graph, zero, add, compare) do
case :yog@pathfinding@floyd_warshall.floyd_warshall(graph, zero, add, compare) do
{:ok, gleam_dict} ->
{:ok, wrap_distance_matrix(gleam_dict)}
{:error, _} ->
{:error, :negative_cycle}
end
end
@doc """
Convenience function for integer weights.
"""
@spec floyd_warshall_int(Yog.graph()) ::
{:ok, %{required({Yog.node_id(), Yog.node_id()}) => integer()}}
| {:error, :negative_cycle}
def floyd_warshall_int(graph) do
case :yog@pathfinding@floyd_warshall.floyd_warshall_int(graph) do
{:ok, gleam_dict} -> {:ok, wrap_distance_matrix(gleam_dict)}
{:error, _} -> {:error, :negative_cycle}
end
end
@doc """
Convenience function for float weights.
"""
@spec floyd_warshall_float(Yog.graph()) ::
{:ok, %{required({Yog.node_id(), Yog.node_id()}) => float()}}
| {:error, :negative_cycle}
def floyd_warshall_float(graph) do
case :yog@pathfinding@floyd_warshall.floyd_warshall_float(graph) do
{:ok, gleam_dict} -> {:ok, wrap_distance_matrix(gleam_dict)}
{:error, _} -> {:error, :negative_cycle}
end
end
@doc """
Detects whether the graph contains a negative cycle.
More efficient than running the full algorithm if you only need cycle detection.
"""
@spec detect_negative_cycle?(
Yog.graph(),
any(),
(any(), any() -> any()),
(any(), any() -> :lt | :eq | :gt)
) :: boolean()
def detect_negative_cycle?(graph, zero, add, compare) do
:yog@pathfinding@floyd_warshall.detect_negative_cycle(graph, zero, add, compare)
end
# Private helper to wrap Gleam distance matrix
defp wrap_distance_matrix(gleam_dict) do
gleam_dict
|> :gleam@dict.to_list()
|> Map.new()
end
end