Skip to main content

sim_lib_discrete_graph/
path.rs

1//! Shortest paths: single-source Dijkstra and Bellman-Ford, plus all-pairs and
2//! reachability as thin wrappers over the algebra spine's semiring closure.
3
4use crate::error::GraphError;
5use crate::graph::Graph;
6use core::cmp::Reverse;
7use sim_lib_discrete_algebra::{AlgebraLimits, BoolRing, Matrix, MinPlus};
8use std::collections::BinaryHeap;
9
10/// Single-source distances and predecessor forest.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PathResult<W> {
13    /// `distances[v]` is the shortest distance to `v`, or `None` if unreachable.
14    pub distances: Vec<Option<W>>,
15    /// `predecessors[v]` is the node `v` was reached from on a shortest path.
16    pub predecessors: Vec<Option<usize>>,
17}
18
19/// Directed out-arcs `(target, weight)` of `node`, honoring directedness.
20fn out_arcs<N, W: Clone>(graph: &Graph<N, W>, node: usize) -> Vec<(usize, W)> {
21    let undirected = !graph.is_directed();
22    let mut arcs = Vec::new();
23    for e in &graph.edges {
24        if e.source == node {
25            arcs.push((e.target, e.weight.clone()));
26        } else if undirected && e.target == node {
27            arcs.push((e.source, e.weight.clone()));
28        }
29    }
30    arcs
31}
32
33/// Dijkstra's algorithm over non-negative `u64` weights.
34///
35/// # Examples
36///
37/// On a directed graph where the two-hop route `0 -> 1 -> 2` (1 + 2 = 3) beats
38/// the direct edge `0 -> 2` (5), the shortest distance to node `2` is `3`:
39///
40/// ```
41/// use sim_lib_discrete_graph::{dijkstra, Directedness, Graph};
42///
43/// let mut g: Graph<(), u64> = Graph::with_nodes(vec![(), (), ()], Directedness::Directed);
44/// g.add_edge(0, 1, 1).unwrap();
45/// g.add_edge(1, 2, 2).unwrap();
46/// g.add_edge(0, 2, 5).unwrap();
47///
48/// let r = dijkstra(&g, 0).unwrap();
49/// assert_eq!(r.distances, vec![Some(0), Some(1), Some(3)]);
50/// assert_eq!(r.predecessors[2], Some(1)); // reached via node 1
51/// ```
52pub fn dijkstra<N>(graph: &Graph<N, u64>, source: usize) -> Result<PathResult<u64>, GraphError> {
53    graph.validate()?;
54    let n = graph.node_count();
55    if source >= n {
56        return Err(GraphError::NodeOutOfRange {
57            node: source,
58            count: n,
59        });
60    }
61    let mut dist = vec![None; n];
62    let mut pred = vec![None; n];
63    let mut heap: BinaryHeap<Reverse<(u64, usize)>> = BinaryHeap::new();
64    dist[source] = Some(0);
65    heap.push(Reverse((0, source)));
66    while let Some(Reverse((d, u))) = heap.pop() {
67        if dist[u].is_some_and(|best| d > best) {
68            continue;
69        }
70        for (v, w) in out_arcs(graph, u) {
71            let nd = d + w;
72            if dist[v].is_none_or(|best| nd < best) {
73                dist[v] = Some(nd);
74                pred[v] = Some(u);
75                heap.push(Reverse((nd, v)));
76            }
77        }
78    }
79    Ok(PathResult {
80        distances: dist,
81        predecessors: pred,
82    })
83}
84
85/// Bellman-Ford over `i64` weights. Returns the result and whether a
86/// negative-weight cycle is reachable from the source.
87pub fn bellman_ford<N>(
88    graph: &Graph<N, i64>,
89    source: usize,
90) -> Result<(PathResult<i64>, bool), GraphError> {
91    graph.validate()?;
92    let n = graph.node_count();
93    if source >= n {
94        return Err(GraphError::NodeOutOfRange {
95            node: source,
96            count: n,
97        });
98    }
99    let undirected = !graph.is_directed();
100    // Collect all directed arcs once.
101    let mut arcs: Vec<(usize, usize, i64)> = Vec::new();
102    for e in &graph.edges {
103        arcs.push((e.source, e.target, e.weight));
104        if undirected {
105            arcs.push((e.target, e.source, e.weight));
106        }
107    }
108    let mut dist: Vec<Option<i64>> = vec![None; n];
109    let mut pred = vec![None; n];
110    dist[source] = Some(0);
111    for _ in 0..n.saturating_sub(1) {
112        let mut changed = false;
113        for &(a, b, w) in &arcs {
114            if let Some(da) = dist[a] {
115                let nd = da + w;
116                if dist[b].is_none_or(|best| nd < best) {
117                    dist[b] = Some(nd);
118                    pred[b] = Some(a);
119                    changed = true;
120                }
121            }
122        }
123        if !changed {
124            break;
125        }
126    }
127    let mut negative_cycle = false;
128    for &(a, b, w) in &arcs {
129        if let Some(da) = dist[a]
130            && dist[b].is_none_or(|best| da + w < best)
131        {
132            negative_cycle = true;
133            break;
134        }
135    }
136    Ok((
137        PathResult {
138            distances: dist,
139            predecessors: pred,
140        },
141        negative_cycle,
142    ))
143}
144
145/// All-pairs shortest paths as the min-plus closure of the adjacency matrix.
146/// This is a thin wrapper over the spine; it does not re-implement Floyd-Warshall.
147pub fn all_pairs_shortest_paths<N>(graph: &Graph<N, i64>) -> Result<Matrix<MinPlus>, GraphError> {
148    graph.validate()?;
149    let n = graph.node_count();
150    let undirected = !graph.is_directed();
151    let mut m = Matrix::filled(n, n, MinPlus::Inf);
152    for e in &graph.edges {
153        // Accumulate by semiring add (min) so parallel edges keep the cheapest.
154        m.data[e.source * n + e.target] = min_plus_add(m.data[e.source * n + e.target], e.weight);
155        if undirected {
156            m.data[e.target * n + e.source] =
157                min_plus_add(m.data[e.target * n + e.source], e.weight);
158        }
159    }
160    Ok(m.closure(AlgebraLimits::default())?)
161}
162
163fn min_plus_add(cur: MinPlus, w: i64) -> MinPlus {
164    use sim_lib_discrete_algebra::Semiring;
165    cur.add(&MinPlus::Fin(w))
166}
167
168/// Reachability as the boolean closure of the adjacency matrix. Thin wrapper.
169pub fn reachability<N, W>(graph: &Graph<N, W>) -> Result<Matrix<BoolRing>, GraphError> {
170    graph.validate()?;
171    let n = graph.node_count();
172    let undirected = !graph.is_directed();
173    let mut m = Matrix::filled(n, n, BoolRing(false));
174    for e in &graph.edges {
175        m.data[e.source * n + e.target] = BoolRing(true);
176        if undirected {
177            m.data[e.target * n + e.source] = BoolRing(true);
178        }
179    }
180    Ok(m.closure(AlgebraLimits::default())?)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::edge::Directedness;
187
188    #[test]
189    fn dijkstra_row_equals_all_pairs_row() {
190        // Same structure over u64 (Dijkstra) and i64 (all-pairs closure).
191        let edges = [(0usize, 1usize, 1u64), (1, 2, 2), (0, 2, 5), (2, 3, 1)];
192        let mut gu: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2, 3], Directedness::Directed);
193        let mut gi: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2, 3], Directedness::Directed);
194        for &(s, t, w) in &edges {
195            gu.add_edge(s, t, w).unwrap();
196            gi.add_edge(s, t, w as i64).unwrap();
197        }
198        let dj = dijkstra(&gu, 0).unwrap();
199        let ap = all_pairs_shortest_paths(&gi).unwrap();
200        for j in 0..4 {
201            let from_closure = match ap.data[j] {
202                MinPlus::Fin(d) => Some(d as u64),
203                MinPlus::Inf => None,
204            };
205            assert_eq!(dj.distances[j], from_closure, "node {j}");
206        }
207    }
208
209    #[test]
210    fn bellman_ford_handles_negative_edge_without_cycle() {
211        let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
212        g.add_edge(0, 1, 4).unwrap();
213        g.add_edge(0, 2, 5).unwrap();
214        g.add_edge(2, 1, -3).unwrap(); // 0->2->1 = 2 beats direct 4
215        let (res, neg) = bellman_ford(&g, 0).unwrap();
216        assert!(!neg);
217        assert_eq!(res.distances[1], Some(2));
218    }
219
220    #[test]
221    fn bellman_ford_detects_negative_cycle() {
222        let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
223        g.add_edge(0, 1, 1).unwrap();
224        g.add_edge(1, 0, -2).unwrap(); // cycle weight -1
225        let (_res, neg) = bellman_ford(&g, 0).unwrap();
226        assert!(neg);
227    }
228
229    #[test]
230    fn reachability_is_transitive() {
231        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
232        g.add_edge(0, 1, 1).unwrap();
233        g.add_edge(1, 2, 1).unwrap();
234        let r = reachability(&g).unwrap();
235        assert_eq!(r.data[2], BoolRing(true)); // 0 reaches 2
236        assert_eq!(r.data[6], BoolRing(false)); // 2 does not reach 0
237    }
238}