From c168d93582b982bbfc6723fe6f89b86a983ce974 Mon Sep 17 00:00:00 2001 From: tachsin Date: Fri, 11 Sep 2026 21:41:20 +0300 Subject: [PATCH] docs(yen): say what happens when two nodes are joined by more than one edge A path here is a sequence of nodes, so two routes that differ only in which of several edges between the same pair they take are the same path. `yen` reports the cheaper edge and never the others, and so returns fewer paths than asked for on such a graph. That is consistent, but it is not what someone counting routes expects, and nothing said it. Document it, and document the way round it: carrying the edge a node was reached by inside the node makes the routes distinct, and both are then found. That needs nothing from this crate, and is worth writing down because the shape of the answer is not obvious from the signature. Tests cover the graph from the report and the same situation in the middle of a longer path, along with the suggested modelling. --- src/directed/yen.rs | 42 +++++++++++++++++++++++++++++++++++++++++ tests/yen.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/directed/yen.rs b/src/directed/yen.rs index 908a1441..aac0d5ad 100644 --- a/src/directed/yen.rs +++ b/src/directed/yen.rs @@ -98,6 +98,48 @@ where /// 2); /// assert!(empty.is_empty()); /// ``` +/// +/// # Parallel edges +/// +/// A path is a sequence of nodes, so two routes differing only in which of several edges +/// between the same pair of nodes they take are the same path. Where more than one edge joins +/// two nodes, the cheaper one is the one taken, and the others are never reported: on the graph +/// below there is one path from `A` to `B`, not two, and asking for `k = 2` returns one. +/// +/// ``` +/// use pathfinding::prelude::yen; +/// +/// let two_roads = |c: &char| match c { +/// 'A' => vec![('B', 3), ('B', 2)], +/// _ => vec![], +/// }; +/// let routes = yen(&'A', two_roads, |c| *c == 'B', 2); +/// assert_eq!(routes, vec![(vec!['A', 'B'], 2)]); +/// ``` +/// +/// When the edges themselves matter, put enough into the node to tell them apart. Carrying the +/// edge a node was reached by makes the two routes different paths, and both are then found: +/// +/// ``` +/// use pathfinding::prelude::yen; +/// +/// // Two roads from A to B, costing 3 and 2. A node is a town plus the road reached it by, +/// // which the starting town has none of. +/// let roads = [('A', 'B', 3), ('A', 'B', 2)]; +/// let successors = |&(town, _): &(char, Option)| { +/// roads +/// .iter() +/// .enumerate() +/// .filter(move |(_, (from, _, _))| *from == town) +/// .map(|(road, &(_, to, cost))| ((to, Some(road)), cost)) +/// .collect::>() +/// }; +/// +/// let routes = yen(&('A', None), successors, |&(town, _)| town == 'B', 2); +/// assert_eq!(routes.len(), 2); +/// assert_eq!(routes[0], (vec![('A', None), ('B', Some(1))], 2)); +/// assert_eq!(routes[1], (vec![('A', None), ('B', Some(0))], 3)); +/// ``` pub fn yen( start: &N, mut successors: FN, diff --git a/tests/yen.rs b/tests/yen.rs index 82bc65ab..5e5fc482 100644 --- a/tests/yen.rs +++ b/tests/yen.rs @@ -212,3 +212,49 @@ fn parallel_edges_are_not_charged_twice() { vec![(vec!['0', 'a', 'c', 'd'], 3), (vec!['0', 'b', 'c', 'd'], 7)] ); } + +/// The graph from issue #516. A path is a sequence of nodes, so the two edges from `A` to `B` +/// describe one path and not two, and `yen` reports the cheaper of them. +#[test] +fn parallel_edges_describe_one_path() { + let two_roads = |c: &char| match c { + 'A' => vec![('B', 3), ('B', 2)], + _ => vec![], + }; + assert_eq!( + yen(&'A', two_roads, |c| *c == 'B', 2), + vec![(vec!['A', 'B'], 2)] + ); + + // The same holds in the middle of a longer graph: the cheaper edge is the one costed, and + // the dearer one does not become a second path through the same towns. + let longer = |c: &char| match c { + 'A' => vec![('B', 1), ('C', 1)], + 'B' => vec![('D', 5), ('D', 1)], + 'C' => vec![('D', 4)], + _ => vec![], + }; + assert_eq!( + yen(&'A', longer, |c| *c == 'D', 3), + vec![(vec!['A', 'B', 'D'], 2), (vec!['A', 'C', 'D'], 5)] + ); +} + +/// Carrying the edge a node was reached by makes routes over parallel edges distinct, which is +/// the modelling the documentation recommends when the individual edges matter. +#[test] +fn edges_in_the_node_make_parallel_routes_distinct() { + let roads = [('A', 'B', 3), ('A', 'B', 2)]; + let successors = |&(town, _): &(char, Option)| { + roads + .iter() + .enumerate() + .filter(move |(_, (from, _, _))| *from == town) + .map(|(road, &(_, to, cost))| ((to, Some(road)), cost)) + .collect::>() + }; + let routes = yen(&('A', None), successors, |&(town, _)| town == 'B', 2); + assert_eq!(routes.len(), 2, "both roads should be found"); + assert_eq!(routes[0], (vec![('A', None), ('B', Some(1))], 2)); + assert_eq!(routes[1], (vec![('A', None), ('B', Some(0))], 3)); +}