weavatrix_graph/algo/
bellman.rs1use crate::{GraphError, IndexGraphView, Result};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct SignedPath<Node> {
5 nodes: Vec<Node>,
6 total_cost: i64,
7}
8
9impl<Node> SignedPath<Node> {
10 #[must_use]
11 pub fn nodes(&self) -> &[Node] {
12 &self.nodes
13 }
14
15 #[must_use]
16 pub const fn total_cost(&self) -> i64 {
17 self.total_cost
18 }
19
20 #[must_use]
21 pub fn into_nodes(self) -> Vec<Node> {
22 self.nodes
23 }
24}
25
26#[derive(Debug, Clone)]
27pub struct BellmanFord<Node> {
28 source: Node,
29 nodes: Vec<Node>,
30 nodes_by_slot: Vec<Option<Node>>,
31 distances: Vec<i64>,
32 reachable: Vec<bool>,
33 predecessors: Vec<Option<usize>>,
34 node_slot: fn(Node) -> usize,
35}
36
37impl<Node> BellmanFord<Node>
38where
39 Node: Copy + Eq,
40{
41 #[must_use]
42 pub const fn source(&self) -> Node {
43 self.source
44 }
45
46 #[must_use]
47 pub fn nodes(&self) -> &[Node] {
48 &self.nodes
49 }
50
51 #[must_use]
52 pub fn distance_to(&self, node: Node) -> Option<i64> {
53 let slot = (self.node_slot)(node);
54 self.nodes_by_slot
55 .get(slot)
56 .is_some_and(|stored| *stored == Some(node))
57 .then(|| self.reachable[slot])
58 .filter(|reachable| *reachable)
59 .map(|_| self.distances[slot])
60 }
61
62 #[must_use]
63 pub fn predecessor(&self, node: Node) -> Option<Node> {
64 let slot = (self.node_slot)(node);
65 self.nodes_by_slot
66 .get(slot)
67 .is_some_and(|stored| *stored == Some(node))
68 .then(|| self.predecessors[slot])
69 .flatten()
70 .and_then(|predecessor| self.nodes_by_slot[predecessor])
71 }
72
73 #[must_use]
74 pub fn path_to(&self, target: Node) -> Option<SignedPath<Node>> {
75 let total_cost = self.distance_to(target)?;
76 let mut nodes = vec![target];
77 let mut cursor = target;
78 while cursor != self.source {
79 cursor = self.predecessor(cursor)?;
80 nodes.push(cursor);
81 if nodes.len() > self.nodes.len() {
82 return None;
83 }
84 }
85 nodes.reverse();
86 Some(SignedPath { nodes, total_cost })
87 }
88}
89
90pub fn bellman_ford<G, F>(
96 graph: &G,
97 source: G::Node,
98 edge_cost: F,
99) -> Result<Option<BellmanFord<G::Node>>>
100where
101 G: IndexGraphView,
102 F: Fn(G::Edge) -> i64,
103{
104 bellman_ford_filtered(graph, source, |edge| Some(edge_cost(edge)))
105}
106
107pub fn bellman_ford_filtered<G, F>(
113 graph: &G,
114 source: G::Node,
115 edge_cost: F,
116) -> Result<Option<BellmanFord<G::Node>>>
117where
118 G: IndexGraphView,
119 F: Fn(G::Edge) -> Option<i64>,
120{
121 if !graph.contains_node(source) {
122 return Ok(None);
123 }
124 let mut nodes_by_slot = vec![None; graph.node_bound()];
125 let nodes = graph.node_indices().collect::<Vec<_>>();
126 for &node in &nodes {
127 nodes_by_slot[G::node_slot(node)] = Some(node);
128 }
129 let mut edges = Vec::with_capacity(graph.edge_count());
130 for (edge, endpoints) in graph.edge_references() {
131 if let Some(weight) = edge_cost(edge) {
132 edges.push((
133 G::node_slot(endpoints.source()),
134 G::node_slot(endpoints.target()),
135 weight,
136 ));
137 }
138 }
139 let mut distances = vec![0_i64; graph.node_bound()];
140 let mut reachable = vec![false; graph.node_bound()];
141 let mut predecessors = vec![None; graph.node_bound()];
142 reachable[G::node_slot(source)] = true;
143 for _ in 1..nodes.len() {
144 if !relax_all(&edges, &mut distances, &mut reachable, &mut predecessors)? {
145 break;
146 }
147 }
148 reject_negative_cycle(&edges, &distances, &reachable)?;
149
150 Ok(Some(BellmanFord {
151 source,
152 nodes,
153 nodes_by_slot,
154 distances,
155 reachable,
156 predecessors,
157 node_slot: G::node_slot,
158 }))
159}
160
161fn relax_all(
162 edges: &[(usize, usize, i64)],
163 distances: &mut [i64],
164 reachable: &mut [bool],
165 predecessors: &mut [Option<usize>],
166) -> Result<bool> {
167 let mut changed = false;
168 for &(source, target, weight) in edges {
169 if !reachable[source] {
170 continue;
171 }
172 let candidate =
173 distances[source]
174 .checked_add(weight)
175 .ok_or(GraphError::ArithmeticOverflow {
176 operation: "Bellman-Ford edge relaxation",
177 })?;
178 if !reachable[target] || candidate < distances[target] {
179 distances[target] = candidate;
180 reachable[target] = true;
181 predecessors[target] = Some(source);
182 changed = true;
183 }
184 }
185 Ok(changed)
186}
187
188fn reject_negative_cycle(
189 edges: &[(usize, usize, i64)],
190 distances: &[i64],
191 reachable: &[bool],
192) -> Result<()> {
193 for &(source, target, weight) in edges {
194 if !reachable[source] {
195 continue;
196 }
197 let candidate =
198 distances[source]
199 .checked_add(weight)
200 .ok_or(GraphError::ArithmeticOverflow {
201 operation: "Bellman-Ford cycle check",
202 })?;
203 if reachable[target] && candidate < distances[target] {
204 return Err(GraphError::NegativeCycle {
205 algorithm: "Bellman-Ford",
206 });
207 }
208 }
209 Ok(())
210}