yog/pathfinding/johnson

Johnson’s algorithm for all-pairs shortest paths in weighted graphs with negative edge weights.

Johnson’s algorithm efficiently computes shortest paths between all pairs of nodes in sparse graphs, even when edges have negative weights (but no negative cycles). It combines Bellman-Ford and Dijkstra’s algorithms with a reweighting technique.

Algorithm

AlgorithmFunctionComplexityBest For
Johnson’sjohnson/4O(V² log V + VE)Sparse graphs with negative weights

Key Concepts

How Reweighting Works

The algorithm computes a potential function h(v) for each vertex such that:

w'(u,v) = w(u,v) + h(u) - h(v) ≥ 0

This transformation preserves shortest paths because for any path p = v₁→v₂→…→vₖ:

w'(p) = w(p) + h(v₁) - h(vₖ)

So the relative ordering of path weights remains the same!

The Algorithm Steps

  1. Add temporary source: Create new vertex s with 0-weight edges to all vertices
  2. Run Bellman-Ford: From s to compute h(v) = distance[v] and detect negative cycles
  3. Reweight edges: Set w’(u,v) = w(u,v) + h(u) - h(v) for all edges
  4. Run V × Dijkstra: Compute shortest paths on reweighted graph
  5. Adjust distances: Set dist(u,v) = dist’(u,v) - h(u) + h(v)

Comparison with Other All-Pairs Algorithms

ApproachComplexityBest For
Floyd-WarshallO(V³)Dense graphs (E ≈ V²)
Johnson’sO(V² log V + VE)Sparse graphs (E ≪ V²)
V × DijkstraO(V(V+E) log V)Sparse graphs, non-negative weights only
V × Bellman-FordO(V²E)Rarely optimal

Rule of thumb:

Complexity Analysis

For sparse graphs where E = O(V), this is O(V² log V), much better than Floyd-Warshall’s O(V³)!

Negative Cycles

The algorithm detects negative cycles during the Bellman-Ford phase. If a negative cycle exists, the function returns Error(Nil).

Use Cases

History

Published by Donald B. Johnson in 1977. The algorithm is a brilliant combination of Bellman-Ford’s ability to handle negative weights and Dijkstra’s efficiency with non-negative weights.

References

Values

pub fn johnson(
  in graph: model.Graph(n, e),
  with_zero zero: e,
  with_add add: fn(e, e) -> e,
  with_subtract subtract: 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 Johnson’s algorithm.

Time Complexity: O(V² log V + VE)

Returns an error if a negative cycle is detected.

Parameters

  • zero: The identity element for addition (e.g., 0 for integers)
  • add: Function to add two weights
  • subtract: Function to subtract two weights (for reweighting)
  • compare: Function to compare two weights

Example

let result = johnson.johnson(
  in: graph,
  with_zero: 0,
  with_add: int.add,
  with_subtract: int.subtract,
  with_compare: int.compare
)
// => Ok(Dict([#(#(1, 2), 10), #(#(1, 3), 25), ...]))

When to Use

Use Johnson’s algorithm for sparse graphs when you need all-pairs shortest paths and the graph may contain negative edge weights (but no negative cycles). For dense graphs, prefer Floyd-Warshall. For graphs with only non-negative weights, consider running Dijkstra from each vertex directly.

pub fn johnson_float(
  in graph: model.Graph(n, Float),
) -> Result(dict.Dict(#(Int, Int), Float), Nil)

Computes all-pairs shortest paths with float weights using Johnson’s algorithm.

This is a convenience wrapper around johnson that uses:

  • 0.0 as the zero element
  • float.add for addition
  • float.subtract for subtraction
  • float.compare for comparison

Warning

Float arithmetic has precision limitations. Negative cycles might not be detected reliably due to floating-point errors. Prefer Int weights for critical calculations.

pub fn johnson_int(
  in graph: model.Graph(n, Int),
) -> Result(dict.Dict(#(Int, Int), Int), Nil)

Computes all-pairs shortest paths with integer weights using Johnson’s algorithm.

This is a convenience wrapper around johnson that uses:

  • 0 as the zero element
  • int.add for addition
  • int.subtract for subtraction
  • int.compare for comparison

Example

let result = johnson.johnson_int(graph)
// => Ok(Dict([#(#(1, 2), 10), #(#(1, 3), 25), ...]))

When to Use

Use this for sparse graphs where you need all-pairs distances with Int weights that may be negative. For dense graphs, prefer floyd_warshall_int. For graphs with only non-negative weights, consider running dijkstra V times.

Returns Error(Nil) if a negative cycle is detected.

Search Document