From 91fa896e708153c0c8dea0d8885b72c40c142685 Mon Sep 17 00:00:00 2001 From: SOFe Date: Sun, 27 Jul 2025 21:21:28 +0800 Subject: [PATCH] feat: add fallible variants for dijkstra and yen algorithms --- src/directed/dijkstra.rs | 114 ++++++++++++++++++++++++++++++++------- src/directed/yen.rs | 64 ++++++++++++++++------ 2 files changed, 144 insertions(+), 34 deletions(-) diff --git a/src/directed/dijkstra.rs b/src/directed/dijkstra.rs index 86958ebe..daf1042b 100644 --- a/src/directed/dijkstra.rs +++ b/src/directed/dijkstra.rs @@ -8,6 +8,7 @@ use num_traits::Zero; use rustc_hash::{FxHashMap, FxHashSet}; use std::cmp::Ordering; use std::collections::{BinaryHeap, HashMap}; +use std::convert::Infallible; use std::hash::Hash; /// Compute a shortest path using the [Dijkstra search @@ -79,29 +80,56 @@ where FN: FnMut(&N) -> IN, IN: IntoIterator, FS: FnMut(&N) -> bool, +{ + match dijkstra_internal::<_, _, _, _, _, Infallible>( + start, + &mut |node| Ok(successors(node)), + &mut |node| Ok(success(node)), + ) { + Ok(result) => result, + } +} + +/// Compute a shortest path using the [Dijkstra search +/// algorithm](https://en.wikipedia.org/wiki/Dijkstra's_algorithm). +/// +/// # Errors +/// This is the fallible version of [`dijkstra`]. +/// `Err(err)` is returned immediately if and only if `successors` or `success` returns `Err(err)`. +pub fn try_dijkstra( + start: &N, + mut successors: FN, + mut success: FS, +) -> Result, C)>, E> +where + N: Eq + Hash + Clone, + C: Zero + Ord + Copy, + FN: FnMut(&N) -> Result, + IN: IntoIterator, + FS: FnMut(&N) -> Result, { dijkstra_internal(start, &mut successors, &mut success) } -pub(crate) fn dijkstra_internal( +pub(crate) fn dijkstra_internal( start: &N, successors: &mut FN, success: &mut FS, -) -> Option<(Vec, C)> +) -> Result, C)>, E> where N: Eq + Hash + Clone, C: Zero + Ord + Copy, - FN: FnMut(&N) -> IN, + FN: FnMut(&N) -> Result, IN: IntoIterator, - FS: FnMut(&N) -> bool, + FS: FnMut(&N) -> Result, { - let (parents, reached) = run_dijkstra(start, successors, success); - reached.map(|target| { + let (parents, reached) = run_dijkstra(start, successors, success)?; + Ok(reached.map(|target| { ( reverse_path(&parents, |&(p, _)| p, target), parents.get_index(target).unwrap().1.1, ) - }) + })) } /// Determine all reachable nodes from a starting point as well as the @@ -152,6 +180,24 @@ where dijkstra_partial(start, successors, |_| false).0 } +/// Determine all reachable nodes from a starting point as well as the +/// minimum cost to reach them and a possible optimal parent node +/// using the [Dijkstra search +/// algorithm](https://en.wikipedia.org/wiki/Dijkstra's_algorithm). +/// +/// # Errors +/// This is the fallible version of [`dijkstra_all`]. +/// `Err(err)` is returned immediately if and only if `successors` or `success` returns `Err(err)`. +pub fn try_dijkstra_all(start: &N, successors: FN) -> Result, E> +where + N: Eq + Hash + Clone, + C: Zero + Ord + Copy, + FN: FnMut(&N) -> Result, + IN: IntoIterator, +{ + try_dijkstra_partial(start, successors, |_| Ok(false)).map(|(map, _)| map) +} + /// Determine some reachable nodes from a starting point as well as the minimum cost to /// reach them and a possible optimal parent node /// using the [Dijkstra search algorithm](https://en.wikipedia.org/wiki/Dijkstra's_algorithm). @@ -168,7 +214,6 @@ where /// /// The [`build_path`] function can be used to build a full path from the starting point to one /// of the reachable targets. -#[expect(clippy::missing_panics_doc)] pub fn dijkstra_partial( start: &N, mut successors: FN, @@ -181,28 +226,61 @@ where IN: IntoIterator, FS: FnMut(&N) -> bool, { - let (parents, reached) = run_dijkstra(start, &mut successors, &mut stop); - ( + match try_dijkstra_partial::<_, _, _, _, _, Infallible>( + start, + |node| Ok(successors(node)), + |node| Ok(stop(node)), + ) { + Ok(result) => result, + } +} + +type PartialResult = (HashMap, Option); + +/// Determine some reachable nodes from a starting point as well as the minimum cost to +/// reach them and a possible optimal parent node +/// using the [Dijkstra search algorithm](https://en.wikipedia.org/wiki/Dijkstra's_algorithm). +/// +/// # Errors +/// This is the fallible version of [`dijkstra_partial`]. +/// `Err(err)` is returned immediately if and only if `successors` or `success` returns `Err(err)`. +#[expect(clippy::missing_panics_doc)] +pub fn try_dijkstra_partial( + start: &N, + mut successors: FN, + mut stop: FS, +) -> Result, E> +where + N: Eq + Hash + Clone, + C: Zero + Ord + Copy, + FN: FnMut(&N) -> Result, + IN: IntoIterator, + FS: FnMut(&N) -> Result, +{ + let (parents, reached) = run_dijkstra(start, &mut successors, &mut stop)?; + Ok(( parents .iter() .skip(1) .map(|(n, (p, c))| (n.clone(), (parents.get_index(*p).unwrap().0.clone(), *c))) // unwrap() cannot fail .collect(), reached.map(|i| parents.get_index(i).unwrap().0.clone()), - ) + )) } -fn run_dijkstra( +type RunResult = (FxIndexMap, Option); + +fn run_dijkstra( start: &N, successors: &mut FN, stop: &mut FS, -) -> (FxIndexMap, Option) +) -> Result, E> where N: Eq + Hash + Clone, C: Zero + Ord + Copy, - FN: FnMut(&N) -> IN, + FN: FnMut(&N) -> Result, IN: IntoIterator, - FS: FnMut(&N) -> bool, + FS: FnMut(&N) -> Result, { let mut to_see = BinaryHeap::new(); to_see.push(SmallestHolder { @@ -215,7 +293,7 @@ where while let Some(SmallestHolder { cost, index }) = to_see.pop() { let successors = { let (node, &(_, c)) = parents.get_index(index).unwrap(); - if stop(node) { + if stop(node)? { target_reached = Some(index); break; } @@ -225,7 +303,7 @@ where if cost > c { continue; } - successors(node) + successors(node)? }; for (successor, move_cost) in successors { let new_cost = cost + move_cost; @@ -251,7 +329,7 @@ where }); } } - (parents, target_reached) + Ok((parents, target_reached)) } /// Build a path leading to a target according to a parents map, which must diff --git a/src/directed/yen.rs b/src/directed/yen.rs index 57235442..6295e17c 100644 --- a/src/directed/yen.rs +++ b/src/directed/yen.rs @@ -5,6 +5,7 @@ use std::cmp::Ordering; use std::cmp::Reverse; use std::collections::BinaryHeap; use std::collections::HashSet; +use std::convert::Infallible; use std::hash::Hash; use super::dijkstra::dijkstra_internal; @@ -42,6 +43,7 @@ where } } } + /// Compute the k-shortest paths using the [Yen's search /// algorithm](https://en.wikipedia.org/wiki/Yen%27s_algorithm). /// @@ -108,8 +110,37 @@ where IN: IntoIterator, FS: FnMut(&N) -> bool, { - let Some((n, c)) = dijkstra_internal(start, &mut successors, &mut success) else { - return vec![]; + match try_yen::<_, _, _, _, _, Infallible>( + start, + |node| Ok(successors(node)), + |node| Ok(success(node)), + k, + ) { + Ok(v) => v, + } +} + +/// Compute the k-shortest paths using the [Yen's search +/// algorithm](https://en.wikipedia.org/wiki/Yen%27s_algorithm). +/// +/// # Errors +/// This is the fallible version of [`yen`]. +/// `Err(err)` is returned immediately if and only if `successors` or `success` returns `Err(err)`. +pub fn try_yen( + start: &N, + mut successors: FN, + mut success: FS, + k: usize, +) -> Result, C)>, E> +where + N: Eq + Hash + Clone, + C: Zero + Ord + Copy, + FN: FnMut(&N) -> Result, + IN: IntoIterator, + FS: FnMut(&N) -> Result, +{ + let Some((n, c)) = dijkstra_internal(start, &mut successors, &mut success)? else { + return Ok(vec![]); }; let mut visited = HashSet::new(); @@ -142,23 +173,24 @@ where // We are creating a new successor function that will not return the // filtered edges and nodes that routes already used. let mut filtered_successor = |n: &N| { - successors(n) - .into_iter() - .filter(|(n2, _)| { - !filtered_nodes.contains(&n2) && !filtered_edges.contains(&(n, n2)) - }) - .collect::>() + successors(n).map(|list| { + list.into_iter() + .filter(|(n2, _)| { + !filtered_nodes.contains(&n2) && !filtered_edges.contains(&(n, n2)) + }) + .collect::>() + }) }; // Let us find the spur path from the spur node to the sink using. if let Some((spur_path, _)) = - dijkstra_internal(spur_node, &mut filtered_successor, &mut success) + dijkstra_internal(spur_node, &mut filtered_successor, &mut success)? { let nodes: Vec = root_path.iter().cloned().chain(spur_path).collect(); // If we have found the same path before, we will not add it. if !visited.contains(&nodes) { // Since we don't know the root_path cost, we need to recalculate. - let cost = make_cost(&nodes, &mut successors); + let cost = make_cost(&nodes, &mut successors)?; let path = Path { nodes, cost }; // Mark as visited visited.insert(path.nodes.clone()); @@ -190,26 +222,26 @@ where } routes.sort_unstable(); - routes + Ok(routes .into_iter() .map(|Path { nodes, cost }| (nodes, cost)) - .collect() + .collect()) } -fn make_cost(nodes: &[N], successors: &mut FN) -> C +fn make_cost(nodes: &[N], successors: &mut FN) -> Result where N: Eq, C: Zero, - FN: FnMut(&N) -> IN, + FN: FnMut(&N) -> Result, IN: IntoIterator, { let mut cost = C::zero(); for edge in nodes.windows(2) { - for (n, c) in successors(&edge[0]) { + for (n, c) in successors(&edge[0])? { if n == edge[1] { cost = cost + c; } } } - cost + Ok(cost) }