Shortest paths (negative weights)¶
Most of graphfinder is GENERAL-SEARCH: a goal-directed walk over a frontier (Algorithms). Dijkstra and A* are the optimal members of that family — but they assume every edge cost is non-negative. The moment a weight can be negative (a refund, an elevation drop, a reward), they can return the wrong answer.
For that case graphfinder ships the two classic relaxation / dynamic programming algorithms. They are different in kind: instead of stopping at a goal, they compute all the distances from a source (or between every pair), and they handle negative edges and detect negative cycles.
| Function | Computes | Negative edges | Negative cycle | Time |
|---|---|---|---|---|
bellman_ford |
one source → all | yes | yes (from source) | O(V·E) |
floyd_warshall |
all pairs | yes | yes (anywhere) | O(V³) |
Both take an edge list over 0..num_nodes ((u, v, w) triples), exactly
like search_graph. They default to directed edges
(undirected=False) — an undirected negative edge is itself a trivial negative
cycle, so directed graphs are the meaningful setting.
When to reach for these vs. Dijkstra/A*
Use ucs/dijkstra or astar for a single
start→goal query on a non-negative graph — they stop early and (with a
heuristic) explore far less. Use Bellman–Ford when some edge can be
negative, or when you need to detect a negative cycle. Use
Floyd–Warshall when you want the whole distance matrix at once.
Bellman–Ford — single source¶
import graphfinder as gf
# 0 →(4)→ 1, 0 →(5)→ 2, 1 →(-3)→ 2
# The cheapest 0→2 path goes through 1: 4 + (-3) = 1, beating the direct 5.
edges = [(0, 1, 4.0), (0, 2, 5.0), (1, 2, -3.0)]
sp = gf.bellman_ford(num_nodes=3, edges=edges, source=0)
sp.dist # [0.0, 4.0, 1.0] cheapest cost from the source
sp.path_to(2) # [0, 1, 2] rebuilt shortest path
sp.pred # [None, 0, 1] predecessor on each shortest path
sp.negative_cycle # False
dist[v] is inf for unreachable nodes, and path_to(v) then returns None.
Detecting a negative cycle¶
If a cycle of net-negative weight is reachable from the source, no finite shortest path is well defined. Bellman–Ford reports it:
# 0 → 1 → 2 → 0 sums to -1.
cyc = gf.bellman_ford(3, [(0, 1, 1.0), (1, 2, -3.0), (2, 0, 1.0)], source=0)
cyc.negative_cycle # True
Floyd–Warshall — all pairs¶
edges = [(0, 1, 4.0), (0, 2, 5.0), (1, 2, -3.0), (2, 3, 2.0)]
ap = gf.floyd_warshall(num_nodes=4, edges=edges)
ap.distance(0, 3) # 3.0 cheapest 0→3 cost
ap.path(0, 3) # [0, 1, 2, 3] rebuilt path
ap.matrix() # full 4×4 distance matrix (rows of floats; inf = no path)
ap.negative_cycle # False True if any vertex can reach itself at < 0
O(V³) time and O(V²) memory: ideal for small/medium or dense graphs where
you want every distance, impractical for very large sparse ones (run
Bellman–Ford or Dijkstra per source instead).
Rust¶
The same two functions live in graphfinder_core::shortest_paths, operating on a
CsrGraph:
use graphfinder_core::{CsrGraph, bellman_ford, floyd_warshall};
let g = CsrGraph::from_edges(3, &[(0, 1, 4.0), (0, 2, 5.0), (1, 2, -3.0)], false);
let sp = bellman_ford(&g, 0);
assert_eq!(sp.dist[2], 1.0);
assert_eq!(sp.path_to(2), Some(vec![0, 1, 2]));
let ap = floyd_warshall(&g);
assert_eq!(ap.distance(0, 2), 1.0);
See the API reference for the full ShortestPaths and
AllPairs surface.