Add depth-first search algorithms and tests

This commit is contained in:
2026-06-26 15:34:42 +02:00
parent 91ac925417
commit 135f256141
4 changed files with 289 additions and 0 deletions
+74
View File
@@ -186,3 +186,77 @@ where
} }
(distances, None) (distances, None)
} }
pub struct DfsResult<V: Copy> {
pub visited: VertexMap<V, bool>,
pub predecessors: VertexMap<V, Option<V>>,
}
pub fn dfs<G>(graph: &G, source: G::Vertex) -> DfsResult<G::Vertex>
where
G: GraphTopology,
{
let mut predecessors = graph.vertex_map(None);
let (visited, _) = dfs_impl(graph, source, |neighbor, predecessor| {
predecessors[neighbor] = Some(predecessor);
true
});
DfsResult {
visited,
predecessors,
}
}
pub fn dfs_visited<G>(graph: &G, source: G::Vertex) -> VertexMap<G::Vertex, bool>
where
G: GraphTopology,
{
dfs_impl(graph, source, |_, _| true).0
}
pub fn dfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> bool
where
G: GraphTopology,
{
dfs_find_where(graph, source, |v| v == target).is_some()
}
pub fn dfs_find_where<G, P>(graph: &G, source: G::Vertex, predicate: P) -> Option<G::Vertex>
where
G: GraphTopology,
P: Fn(G::Vertex) -> bool,
{
if predicate(source) {
return Some(source);
}
dfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).1
}
fn dfs_impl<G, F>(
graph: &G,
source: G::Vertex,
mut on_discover: F,
) -> (VertexMap<G::Vertex, bool>, Option<G::Vertex>)
where
G: GraphTopology,
F: FnMut(G::Vertex, G::Vertex) -> bool,
{
let mut visited = graph.vertex_map(false);
visited[source] = true;
let mut stack = vec![(source, None::<G::Vertex>)];
while let Some((v, predecessor)) = stack.pop() {
if let Some(p) = predecessor {
if !on_discover(v, p) {
return (visited, Some(v));
}
}
for neighbor in graph.adjacent_vertices(v) {
if !visited[neighbor] {
visited[neighbor] = true;
stack.push((neighbor, Some(v)));
}
}
}
(visited, None)
}
+1
View File
@@ -1,4 +1,5 @@
pub(crate) mod bfs_testing; pub(crate) mod bfs_testing;
pub(crate) mod dfs_testing;
pub(crate) mod dijkstra_testing; pub(crate) mod dijkstra_testing;
pub(crate) mod graph_topology_testing; pub(crate) mod graph_topology_testing;
pub(crate) mod maps_testing; pub(crate) mod maps_testing;
+201
View File
@@ -0,0 +1,201 @@
#[macro_export]
macro_rules! dfs_tests {
($T:ty) => {
#[test]
fn dfs_single_vertex() {
use $crate::traits::GraphTopology;
let mut graph = <$T>::new();
let v = graph.add_vertex();
let result = $crate::algorithms::dfs(&graph, v);
assert!(result.visited[v], "source vertex should be visited");
assert_eq!(
result.predecessors[v], None,
"unexpected predecessor of source vertex"
);
}
#[test]
fn dfs_disconnected() {
let (graph, vertices) = make_test_graph_disconnected();
let result = $crate::algorithms::dfs(&graph, vertices[0]);
assert!(
result.visited[vertices[0]],
"source vertex should be visited"
);
assert_eq!(
result.predecessors[vertices[0]], None,
"unexpected predecessor of source vertex"
);
for &v in &vertices[1..3] {
assert!(
!result.visited[v],
"disconnected vertex {v:?} should not be visited"
);
assert_eq!(
result.predecessors[v], None,
"disconnected vertex {v:?} should have no predecessor"
);
}
}
#[test]
fn dfs() {
let (graph, vertices, _, _) = make_test_graph();
let result = $crate::algorithms::dfs(&graph, vertices[0]);
assert_dfs_visited(&result.visited, &vertices);
assert_dfs_predecessors(&graph, &result.visited, &result.predecessors, &vertices);
}
#[test]
fn dfs_visited_single_vertex() {
use $crate::traits::GraphTopology;
let mut graph = <$T>::new();
let v = graph.add_vertex();
let visited = $crate::algorithms::dfs_visited(&graph, v);
assert!(visited[v], "source vertex should be visited");
}
#[test]
fn dfs_visited_disconnected() {
let (graph, vertices) = make_test_graph_disconnected();
let visited = $crate::algorithms::dfs_visited(&graph, vertices[0]);
assert!(visited[vertices[0]], "source vertex should be visited");
for &v in &vertices[1..3] {
assert!(
!visited[v],
"disconnected vertex {v:?} should not be visited"
);
}
}
#[test]
fn dfs_visited() {
let (graph, vertices, _, _) = make_test_graph();
let visited = $crate::algorithms::dfs_visited(&graph, vertices[0]);
assert_dfs_visited(&visited, &vertices);
}
#[test]
fn dfs_find_source() {
use $crate::traits::GraphTopology;
let mut graph = <$T>::new();
let v = graph.add_vertex();
assert!(
$crate::algorithms::dfs_find(&graph, v, v),
"source should find itself"
);
}
#[test]
fn dfs_find_disconnected() {
let (graph, vertices) = make_test_graph_disconnected();
assert!(
!$crate::algorithms::dfs_find(&graph, vertices[0], vertices[1]),
"disconnected target should not be found"
);
}
#[test]
fn dfs_find() {
let (graph, vertices, _, _) = make_test_graph();
for i in 0..10 {
assert!(
$crate::algorithms::dfs_find(&graph, vertices[0], vertices[i]),
"vertex {:?} should be reachable from {:?}",
vertices[i],
vertices[0]
);
}
}
#[test]
fn dfs_find_where_source_matches() {
use $crate::traits::GraphTopology;
let mut graph = <$T>::new();
let v = graph.add_vertex();
assert_eq!(
$crate::algorithms::dfs_find_where(&graph, v, |u| u == v),
Some(v),
"source should find itself"
);
}
#[test]
fn dfs_find_where_disconnected() {
let (graph, vertices) = make_test_graph_disconnected();
assert_eq!(
$crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]),
None,
"disconnected vertex {:?} should not be found",
vertices[1]
);
}
#[test]
fn dfs_find_where_no_match() {
let (graph, vertices, _, _) = make_test_graph();
assert_eq!(
$crate::algorithms::dfs_find_where(&graph, vertices[0], |_| false),
None,
"no vertex should match an always-false predicate"
);
}
#[test]
fn dfs_find_where_adjacent() {
let (graph, vertices, _, _) = make_test_graph();
assert_eq!(
$crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]),
Some(vertices[1]),
"expected to find adjacent vertex"
);
}
#[test]
fn dfs_find_where() {
let (graph, vertices, _, _) = make_test_graph();
assert_eq!(
$crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[9]),
Some(vertices[9]),
"expected to find connected vertex");
}
fn assert_dfs_visited(
visited: &$crate::maps::VertexMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>,
vertices: &[<$T as $crate::traits::GraphTopology>::Vertex],
) {
for i in 0..10 {
assert!(
visited[vertices[i]],
"vertex {:?} should be visited",
vertices[i]
);
}
}
fn assert_dfs_predecessors(
graph: &$T,
visited: &$crate::maps::VertexMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>,
predecessors: &$crate::maps::VertexMap<
<$T as $crate::traits::GraphTopology>::Vertex,
Option<<$T as $crate::traits::GraphTopology>::Vertex>,
>,
vertices: &[<$T as $crate::traits::GraphTopology>::Vertex],
) {
use $crate::traits::GraphTopology;
assert_eq!(
predecessors[vertices[0]], None,
"source should have no predecessor"
);
for i in 1..10 {
let v = vertices[i];
let p = predecessors[v].expect(&format!("vertex {v:?} should have a predecessor"));
assert!(visited[p], "predecessor {p:?} of vertex {v:?} should be visited");
assert!(
graph.are_adjacent(v, p),
"predecessor {p:?} of vertex {v:?} should be adjacent"
);
}
}
};
}
+13
View File
@@ -0,0 +1,13 @@
mod append_graph_tests {
use grapherity::models::append_graph::AppendGraph;
grapherity::graph_topology_test_fixtures!(AppendGraph);
grapherity::dfs_tests!(AppendGraph);
}
mod graph_tests {
use grapherity::models::graph::Graph;
grapherity::graph_topology_test_fixtures!(Graph);
grapherity::dfs_tests!(Graph);
}