diff --git a/src/algorithms.rs b/src/algorithms.rs index 4a7e57d..d2000d5 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -186,3 +186,77 @@ where } (distances, None) } + +pub struct DfsResult { + pub visited: VertexMap, + pub predecessors: VertexMap>, +} + +pub fn dfs(graph: &G, source: G::Vertex) -> DfsResult +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(graph: &G, source: G::Vertex) -> VertexMap +where + G: GraphTopology, +{ + dfs_impl(graph, source, |_, _| true).0 +} + +pub fn dfs_find(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(graph: &G, source: G::Vertex, predicate: P) -> Option +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( + graph: &G, + source: G::Vertex, + mut on_discover: F, +) -> (VertexMap, Option) +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::)]; + + 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) +} diff --git a/src/testing.rs b/src/testing.rs index c262cb1..c8ded87 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,4 +1,5 @@ pub(crate) mod bfs_testing; +pub(crate) mod dfs_testing; pub(crate) mod dijkstra_testing; pub(crate) mod graph_topology_testing; pub(crate) mod maps_testing; diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs new file mode 100644 index 0000000..255445d --- /dev/null +++ b/src/testing/dfs_testing.rs @@ -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" + ); + } + } + }; +} diff --git a/tests/dfs.rs b/tests/dfs.rs new file mode 100644 index 0000000..4524c57 --- /dev/null +++ b/tests/dfs.rs @@ -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); +}