graph_shortest_path/bellman_ford.rs
1use graph_core::{Graph, GraphError, NodeId};
2use std::collections::HashMap;
3
4/// Result of a Bellman-Ford shortest-path search.
5pub struct BellmanFordResult {
6 /// Map from `NodeId` to the shortest distance from the source.
7 ///
8 /// Nodes not reachable from the source are absent.
9 pub distances: HashMap<NodeId, f64>,
10 /// Parent of each node in the shortest-path tree.
11 pub parents: HashMap<NodeId, NodeId>,
12}
13
14/// Runs the Bellman-Ford algorithm from `source`.
15///
16/// Unlike Dijkstra, Bellman-Ford handles **negative edge weights**. It detects
17/// negative-weight cycles reachable from `source` and returns an error if one
18/// exists.
19///
20/// # Algorithm
21///
22/// Relax all edges V-1 times. On the V-th relaxation pass, if any distance
23/// can still be improved a negative-weight cycle exists.
24///
25/// # Errors
26///
27/// - [`GraphError::NodeNotFound`] if `source` is not in the graph.
28/// - [`GraphError::NegativeCycle`] if a negative-weight cycle is reachable.
29///
30/// # Complexity
31///
32/// O(V · E).
33///
34/// # Examples
35///
36/// ```
37/// use graph_core::{AdjacencyList, Graph};
38/// use graph_shortest_path::bellman_ford;
39///
40/// let mut g: AdjacencyList<()> = AdjacencyList::directed();
41/// let a = g.add_node(());
42/// let b = g.add_node(());
43/// let c = g.add_node(());
44/// g.add_edge(a, b, 4.0).unwrap();
45/// g.add_edge(a, c, 2.0).unwrap();
46/// g.add_edge(c, b, -1.0).unwrap(); // negative weight, but no negative cycle
47///
48/// let result = bellman_ford(&g, a).unwrap();
49/// assert_eq!(result.distances[&b], 1.0); // a→c→b: 2 + (-1) = 1
50/// ```
51pub fn bellman_ford<G>(graph: &G, source: NodeId) -> Result<BellmanFordResult, GraphError>
52where
53 G: Graph<Weight = f64>,
54{
55 if !graph.contains_node(source) {
56 return Err(GraphError::NodeNotFound(source));
57 }
58
59 // Initialise: source = 0, all others = ∞.
60 let mut distances: HashMap<NodeId, f64> = graph
61 .nodes()
62 .map(|n| (n, if n == source { 0.0 } else { f64::INFINITY }))
63 .collect();
64 let mut parents: HashMap<NodeId, NodeId> = HashMap::new();
65
66 let v = graph.node_count();
67
68 // Collect all edges once to avoid repeated iteration over the graph.
69 let edges = graph.all_edges();
70
71 // Relax V-1 times.
72 for _ in 0..v.saturating_sub(1) {
73 let mut updated = false;
74 for edge in &edges {
75 let u_dist = distances[&edge.source];
76 if u_dist == f64::INFINITY {
77 continue;
78 }
79 let candidate = u_dist + edge.weight;
80 if candidate < distances[&edge.target] {
81 distances.insert(edge.target, candidate);
82 parents.insert(edge.target, edge.source);
83 updated = true;
84 }
85 }
86 // Early termination: if no update occurred, we're done.
87 if !updated {
88 break;
89 }
90 }
91
92 // V-th pass: if any relaxation still succeeds, there's a negative cycle.
93 for edge in &edges {
94 let u_dist = distances[&edge.source];
95 if u_dist == f64::INFINITY {
96 continue;
97 }
98 if u_dist + edge.weight < distances[&edge.target] {
99 return Err(GraphError::NegativeCycle);
100 }
101 }
102
103 // Remove unreachable nodes (distance still ∞) from the result map.
104 distances.retain(|_, d| d.is_finite());
105
106 Ok(BellmanFordResult { distances, parents })
107}