From 73af8a16caad5f954d1c185e822a9397601d5354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 09:43:45 +0200 Subject: [PATCH] Add algorithms::bfs_find_path --- src/algorithms.rs | 112 +++++++++++++++++++++++++++++++++++++++++++-- tests/bfs.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 3 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index 7719e0f..ca3fdd4 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -425,10 +425,8 @@ where F: FnMut(G::Vertex, G::Vertex) -> bool, { let mut distances = graph.vertex_map(None); - let mut queue = VecDeque::new(); - distances[source] = Some(0); - queue.push_back(source); + let mut queue = VecDeque::from([source]); while let Some(v) = queue.pop_front() { for neighbor in graph.adjacent_vertices(v) { @@ -451,6 +449,114 @@ where } } +/// [Breadth-first search] from `source` for `target`, returns some shortest path as a sequence of +/// edges. +/// +/// Traverses vertices in `graph` starting from `source` until `target` is found, exploring all +/// neighbors before descending further. Returns some shortest path from `source` to `target` as a +/// sequence of edges in traversal order if `target` is a valid vertex of `graph` connected to +/// `source`, or `None` otherwise. The returned path is empty if and only if `source` equals +/// `target`. +/// +/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*. +/// +/// # Panics +/// +/// Panics if `source` is not a valid vertex of `graph`. +/// +/// # Examples +/// +/// ``` +/// # use grapherity::prelude::*; +/// # use grapherity::algorithms::bfs_find_path; +/// # use grapherity::models::Graph; +/// let mut graph = Graph::new(); +/// let source = graph.add_vertex(); +/// let target = graph.add_vertex(); +/// let e = graph.add_edge(source, target); +/// +/// assert_eq!(bfs_find_path(&graph, source, target), Some(vec![e])); +/// ``` +/// +/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search +pub fn bfs_find_path(graph: &G, source: G::Vertex, target: G::Vertex) -> Option> +where + G: GraphTopology, +{ + bfs_find_path_where(graph, source, |v| v == target) +} + +/// [Breadth-first search] from `source` for a vertex matching `predicate`, returns some shortest +/// path as a sequence of edges. +/// +/// Traverses vertices in `graph` starting from `source` until a vertex satisfying `predicate` is +/// found, exploring all neighbors before descending further. Returns some shortest path from +/// `source` to the first vertex satisfying `predicate` as a sequence of edges in traversal order, +/// or `None` if no such vertex is connected to `source`. The returned path is empty if and only if +/// `source` satisfies `predicate`. +/// +/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*. +/// +/// # Panics +/// +/// Panics if `source` is not a valid vertex of `graph`. +/// +/// # Examples +/// +/// ``` +/// # use grapherity::prelude::*; +/// # use grapherity::algorithms::bfs_find_path_where; +/// # use grapherity::models::Graph; +/// let mut graph = Graph::new(); +/// let source = graph.add_vertex(); +/// let target = graph.add_vertex(); +/// let e = graph.add_edge(source, target); +/// +/// assert_eq!(bfs_find_path_where(&graph, source, |v| v == target), Some(vec![e])); +/// ``` +/// +/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search +pub fn bfs_find_path_where(graph: &G, source: G::Vertex, predicate: P) -> Option> +where + G: GraphTopology, + P: Fn(G::Vertex) -> bool, +{ + if predicate(source) { + return Some(vec![]); + } + + let mut visited = graph.vertex_map(false); + visited[source] = true; + let mut predecessor: ElementMap> = + graph.vertex_map(None); + let mut queue = VecDeque::from([source]); + + while let Some(v) = queue.pop_front() { + for Incidence { + vertex: neighbor, + edge, + } in graph.incidences(v) + { + if !visited[neighbor] { + visited[neighbor] = true; + predecessor[neighbor] = Some((v, edge)); + if predicate(neighbor) { + let mut path = vec![edge]; + let mut current = v; + while let Some((prev, e)) = predecessor[current] { + path.push(e); + current = prev; + } + path.reverse(); + return Some(path); + } + queue.push_back(neighbor); + } + } + } + None +} + // TODO: 'visited' is already encoded in 'predecessors', except for 'source', which is visited, but has no predecessor. /// Return data type for [`dfs`]. pub struct DfsResult { diff --git a/tests/bfs.rs b/tests/bfs.rs index 89278fd..90f63e2 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -20,6 +20,14 @@ macro_rules! bfs_tests { bfs_find_where_disconnected, bfs_find_where_no_match, bfs_find_where_nearest, + bfs_find_path_source_equals_target, + bfs_find_path_disconnected, + bfs_find_path_adjacent, + bfs_find_path, + bfs_find_path_where_source_matches, + bfs_find_path_where_disconnected, + bfs_find_path_where_no_match, + bfs_find_path_where, ); }; @@ -227,3 +235,109 @@ where vertices[8] ); } + +fn bfs_find_path_source_equals_target() +where + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::bfs_find_path(&graph, v, v), + Some(vec![]), + "path from source to itself should be empty" + ); +} + +fn bfs_find_path_disconnected() +where + G::Edge: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::bfs_find_path(&graph, vertices[0], vertices[1]), + None, + "no path should exist to disconnected vertex" + ); +} + +fn bfs_find_path_adjacent() +where + G::Edge: Debug, +{ + let (graph, vertices, e) = G::single_edge(); + let path = algorithms::bfs_find_path(&graph, vertices[0], vertices[1]) + .expect("path should exist between adjacent vertices"); + assert_eq!(path, [e], "path should contain only the connecting edge"); +} + +fn bfs_find_path() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + fixtures::assert_standard_unweighted_distances_v0( + |v| { + let result = algorithms::bfs_find_path(&graph, vertices[0], v); + let path = fixtures::assert_valid_path(&graph, result, vertices[0], &[v]); + Some(u32::try_from(path.len()).unwrap()) + }, + &vertices, + ); +} + +fn bfs_find_path_where_source_matches() +where + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::bfs_find_path_where(&graph, v, |u| u == v), + Some(vec![]), + "path from source to itself should be empty" + ); +} + +fn bfs_find_path_where_disconnected() +where + G::Edge: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::bfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), + None, + "no path should exist to disconnected vertex" + ); +} + +fn bfs_find_path_where_no_match() +where + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::bfs_find_path_where(&graph, vertices[0], |_| false), + None, + "no path should exist when predicate never matches" + ); +} + +fn bfs_find_path_where() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // vertices[7] and vertices[8] are at distance 3 from vertices[0]; vertices[9] is at distance 4. + // BFS must return one shortest path, so it must lead to one of the distance-3 matches. + let result = algorithms::bfs_find_path_where(&graph, vertices[0], |v| { + v == vertices[7] || v == vertices[8] || v == vertices[9] + }); + let path = + fixtures::assert_valid_path(&graph, result, vertices[0], &[vertices[7], vertices[8]]); + assert_eq!( + path.len(), + 3, + "BFS should return shortest path to a matching vertex" + ); +}