use crate::certificate::{ShortestPathCertificate, verify_shortest_paths};
use crate::error::GraphError;
use crate::graph::Graph;
use core::cmp::Reverse;
use sim_lib_discrete_algebra::{AlgebraLimits, BoolRing, Matrix, MinPlus};
use std::collections::BinaryHeap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathResult<W> {
pub distances: Vec<Option<W>>,
pub predecessors: Vec<Option<usize>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShortestPath<N> {
pub source: usize,
pub goal: usize,
pub nodes: Vec<N>,
pub distance: Option<i64>,
pub certificate: ShortestPathCertificate,
}
fn out_arcs<N, W: Clone>(graph: &Graph<N, W>, node: usize) -> Vec<(usize, W)> {
let undirected = !graph.is_directed();
let mut arcs = Vec::new();
for e in &graph.edges {
if e.source == node {
arcs.push((e.target, e.weight.clone()));
} else if undirected && e.target == node {
arcs.push((e.source, e.weight.clone()));
}
}
arcs
}
pub fn dijkstra<N>(graph: &Graph<N, u64>, source: usize) -> Result<PathResult<u64>, GraphError> {
graph.validate()?;
let n = graph.node_count();
if source >= n {
return Err(GraphError::NodeOutOfRange {
node: source,
count: n,
});
}
let mut dist = vec![None; n];
let mut pred = vec![None; n];
let mut heap: BinaryHeap<Reverse<(u64, usize)>> = BinaryHeap::new();
dist[source] = Some(0);
heap.push(Reverse((0, source)));
while let Some(Reverse((d, u))) = heap.pop() {
if dist[u].is_some_and(|best| d > best) {
continue;
}
for (v, w) in out_arcs(graph, u) {
let Some(nd) = d.checked_add(w) else {
continue;
};
if dist[v].is_none_or(|best| nd < best) {
dist[v] = Some(nd);
pred[v] = Some(u);
heap.push(Reverse((nd, v)));
}
}
}
Ok(PathResult {
distances: dist,
predecessors: pred,
})
}
pub fn bellman_ford<N>(
graph: &Graph<N, i64>,
source: usize,
) -> Result<(PathResult<i64>, bool), GraphError> {
graph.validate()?;
let n = graph.node_count();
if source >= n {
return Err(GraphError::NodeOutOfRange {
node: source,
count: n,
});
}
let undirected = !graph.is_directed();
let mut arcs: Vec<(usize, usize, i64)> = Vec::new();
for e in &graph.edges {
arcs.push((e.source, e.target, e.weight));
if undirected {
arcs.push((e.target, e.source, e.weight));
}
}
let mut dist: Vec<Option<i64>> = vec![None; n];
let mut pred = vec![None; n];
dist[source] = Some(0);
for _ in 0..n.saturating_sub(1) {
let mut changed = false;
for &(a, b, w) in &arcs {
if let Some(da) = dist[a] {
let nd = da.checked_add(w).ok_or_else(|| {
GraphError::WeightOverflow("Bellman-Ford relaxation".to_string())
})?;
if dist[b].is_none_or(|best| nd < best) {
dist[b] = Some(nd);
pred[b] = Some(a);
changed = true;
}
}
}
if !changed {
break;
}
}
let mut negative_cycle = false;
for &(a, b, w) in &arcs {
if let Some(da) = dist[a] {
let nd = da.checked_add(w).ok_or_else(|| {
GraphError::WeightOverflow("Bellman-Ford cycle check".to_string())
})?;
if dist[b].is_none_or(|best| nd < best) {
negative_cycle = true;
break;
}
}
}
Ok((
PathResult {
distances: dist,
predecessors: pred,
},
negative_cycle,
))
}
pub fn shortest_path<N: Clone>(
graph: &Graph<N, i64>,
source: usize,
goal: usize,
) -> Result<ShortestPath<N>, GraphError> {
graph.validate()?;
let n = graph.node_count();
for node in [source, goal] {
if node >= n {
return Err(GraphError::NodeOutOfRange { node, count: n });
}
}
let (paths, negative_cycle) = bellman_ford(graph, source)?;
if negative_cycle {
return Err(GraphError::NegativeCycle);
}
let certificate = ShortestPathCertificate {
source,
predecessors: paths.predecessors,
};
verify_shortest_paths(graph, &certificate)?;
let nodes = if paths.distances[goal].is_some() {
let mut reversed = Vec::new();
let mut current = goal;
loop {
reversed.push(graph.nodes[current].clone());
if current == source {
break;
}
current = certificate.predecessors[current].ok_or_else(|| {
GraphError::CertificateInvalid("path predecessor gap".to_string())
})?;
}
reversed.reverse();
reversed
} else {
Vec::new()
};
Ok(ShortestPath {
source,
goal,
nodes,
distance: paths.distances[goal],
certificate,
})
}
pub fn all_pairs_shortest_paths<N>(graph: &Graph<N, i64>) -> Result<Matrix<MinPlus>, GraphError> {
graph.validate()?;
let n = graph.node_count();
let mut m = Matrix::try_filled_with_limits(n, n, MinPlus::Inf, AlgebraLimits::default())?;
for source in 0..n {
let (paths, negative_cycle) = bellman_ford(graph, source)?;
if negative_cycle {
return Err(GraphError::NegativeCycle);
}
for (target, distance) in paths.distances.into_iter().enumerate() {
if let Some(distance) = distance {
m.set(source, target, MinPlus::Fin(distance))?;
}
}
}
Ok(m)
}
pub fn reachability<N, W>(graph: &Graph<N, W>) -> Result<Matrix<BoolRing>, GraphError> {
graph.validate()?;
let n = graph.node_count();
let undirected = !graph.is_directed();
let mut m = Matrix::try_filled_with_limits(n, n, BoolRing(false), AlgebraLimits::default())?;
for e in &graph.edges {
m.data[e.source * n + e.target] = BoolRing(true);
if undirected {
m.data[e.target * n + e.source] = BoolRing(true);
}
}
Ok(m.closure(AlgebraLimits::default())?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::edge::Directedness;
#[test]
fn dijkstra_row_equals_all_pairs_row() {
let edges = [(0usize, 1usize, 1u64), (1, 2, 2), (0, 2, 5), (2, 3, 1)];
let mut gu: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2, 3], Directedness::Directed);
let mut gi: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2, 3], Directedness::Directed);
for &(s, t, w) in &edges {
gu.add_edge(s, t, w).unwrap();
gi.add_edge(s, t, w as i64).unwrap();
}
let dj = dijkstra(&gu, 0).unwrap();
let ap = all_pairs_shortest_paths(&gi).unwrap();
for j in 0..4 {
let from_closure = match ap.data[j] {
MinPlus::Fin(d) => Some(d as u64),
MinPlus::Inf => None,
};
assert_eq!(dj.distances[j], from_closure, "node {j}");
}
}
#[test]
fn bellman_ford_handles_negative_edge_without_cycle() {
let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
g.add_edge(0, 1, 4).unwrap();
g.add_edge(0, 2, 5).unwrap();
g.add_edge(2, 1, -3).unwrap(); let (res, neg) = bellman_ford(&g, 0).unwrap();
assert!(!neg);
assert_eq!(res.distances[1], Some(2));
}
#[test]
fn bellman_ford_detects_negative_cycle() {
let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
g.add_edge(0, 1, 1).unwrap();
g.add_edge(1, 0, -2).unwrap(); let (_res, neg) = bellman_ford(&g, 0).unwrap();
assert!(neg);
}
#[test]
fn near_max_weights_do_not_wrap_distance() {
let mut gu: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
gu.add_edge(0, 1, u64::MAX - 1).unwrap();
gu.add_edge(1, 2, u64::MAX - 1).unwrap();
let dj = dijkstra(&gu, 0).unwrap();
assert_eq!(dj.distances[1], Some(u64::MAX - 1));
assert_eq!(dj.distances[2], None);
let mut gi: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
gi.add_edge(0, 1, i64::MAX - 1).unwrap();
gi.add_edge(1, 2, i64::MAX - 1).unwrap();
assert!(matches!(
bellman_ford(&gi, 0),
Err(GraphError::WeightOverflow(_))
));
}
#[test]
fn all_pairs_shortest_paths_rejects_overflow() {
let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
g.add_edge(0, 1, i64::MAX - 1).unwrap();
g.add_edge(1, 2, i64::MAX - 1).unwrap();
assert!(matches!(
all_pairs_shortest_paths(&g),
Err(GraphError::WeightOverflow(_))
));
}
#[test]
fn all_pairs_shortest_paths_rejects_negative_cycle() {
let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
g.add_edge(0, 1, 1).unwrap();
g.add_edge(1, 0, -2).unwrap();
assert_eq!(all_pairs_shortest_paths(&g), Err(GraphError::NegativeCycle));
}
#[test]
fn bellman_ford_rejects_negative_overflow() {
let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
g.add_edge(0, 1, i64::MIN + 1).unwrap();
g.add_edge(1, 2, -2).unwrap();
assert!(matches!(
bellman_ford(&g, 0),
Err(GraphError::WeightOverflow(_))
));
}
#[test]
fn reachability_is_transitive() {
let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
g.add_edge(0, 1, 1).unwrap();
g.add_edge(1, 2, 1).unwrap();
let r = reachability(&g).unwrap();
assert_eq!(r.data[2], BoolRing(true)); assert_eq!(r.data[6], BoolRing(false)); }
#[test]
fn shortest_path_returns_verified_certificate() {
let mut g = Graph::with_nodes(vec!["start", "via", "goal"], Directedness::Directed);
g.add_edge(0, 1, 1).unwrap();
g.add_edge(1, 2, 1).unwrap();
g.add_edge(0, 2, 5).unwrap();
let path = shortest_path(&g, 0, 2).unwrap();
assert_eq!(path.nodes, vec!["start", "via", "goal"]);
assert_eq!(path.distance, Some(2));
assert_eq!(path.certificate.predecessors, vec![None, Some(0), Some(1)]);
verify_shortest_paths(&g, &path.certificate).unwrap();
}
#[test]
fn shortest_path_reports_unreachable_goal_with_certificate() {
let g = Graph::with_nodes(vec![0, 1], Directedness::Directed);
let path = shortest_path(&g, 0, 1).unwrap();
assert_eq!(path.nodes, Vec::<i32>::new());
assert_eq!(path.distance, None);
assert_eq!(path.certificate.predecessors, vec![None, None]);
verify_shortest_paths(&g, &path.certificate).unwrap();
}
}