diff --git a/codecov.yml b/codecov.yml index cb360a3d..609954c6 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,7 @@ coverage: project: default: target: 95% - threshold: 5% \ No newline at end of file + threshold: 5% + patch: + default: + target: 95% \ No newline at end of file diff --git a/include/graaflib/algorithm/graph_traversal.h b/include/graaflib/algorithm/graph_traversal.h index b9a63f97..6229392a 100644 --- a/include/graaflib/algorithm/graph_traversal.h +++ b/include/graaflib/algorithm/graph_traversal.h @@ -7,20 +7,51 @@ namespace graaf::algorithm { +namespace detail { + +/** + * An edge callback which does nothing. + */ +struct noop_callback { + void operator()(const edge_id_t & /*edge*/) const {} +}; + +/* + * A unary predicate which always returns false, effectively resulting in an + * exhaustive search. + */ +struct exhaustive_search_strategy { + [[nodiscard]] bool operator()(const vertex_id_t /*vertex*/) const { + return false; + } +}; + +} // namespace detail + /** * @brief Traverses the graph, starting at start_vertex, and visits all * reachable vertices in a BFS manner. * * @param graph The graph to traverse. * @param start_vertex Vertex id where the traversal should be started. - * @param callback A callback which is called for each traversed edge. Should - * be invocable with an edge_id_t. + * @param edge_callback A callback which is called for each traversed edge. + * Should be invocable with an edge_id_t. + * @param search_termination_strategy A unary predicate to indicate whether we + * should continue the traversal or not. Traversal continues while this + * predicate returns false. */ -template - requires std::invocable -void breadth_first_traverse(const graph &graph, - vertex_id_t start_vertex, - const CALLBACK_T &callback); +template < + typename V, typename E, graph_type T, + typename EDGE_CALLBACK_T = detail::noop_callback, + typename SEARCH_TERMINATION_STRATEGY_T = detail::exhaustive_search_strategy> + requires std::invocable && + std::is_invocable_r_v +void breadth_first_traverse( + const graph &graph, vertex_id_t start_vertex, + const EDGE_CALLBACK_T &edge_callback, + const SEARCH_TERMINATION_STRATEGY_T &search_termination_strategy = + SEARCH_TERMINATION_STRATEGY_T{}); /** * @brief Traverses the graph, starting at start_vertex, and visits all @@ -28,13 +59,24 @@ void breadth_first_traverse(const graph &graph, * * @param graph The graph to traverse. * @param start_vertex Vertex id where the traversal should be started. - * @param callback A callback which is called for each traversed edge. Should - * be invocable with an edge_id_t. + * @param edge_callback A callback which is called for each traversed edge. + * Should be invocable with an edge_id_t. + * @param search_termination_strategy A unary predicate to indicate whether we + * should continue the traversal or not. Traversal continues while this + * predicate returns false. */ -template - requires std::invocable -void depth_first_traverse(const graph &graph, vertex_id_t start_vertex, - const CALLBACK_T &callback); +template < + typename V, typename E, graph_type T, + typename EDGE_CALLBACK_T = detail::noop_callback, + typename SEARCH_TERMINATION_STRATEGY_T = detail::exhaustive_search_strategy> + requires std::invocable && + std::is_invocable_r_v +void depth_first_traverse( + const graph &graph, vertex_id_t start_vertex, + const EDGE_CALLBACK_T &edge_callback, + const SEARCH_TERMINATION_STRATEGY_T &search_termination_strategy = + SEARCH_TERMINATION_STRATEGY_T{}); } // namespace graaf::algorithm diff --git a/include/graaflib/algorithm/graph_traversal.tpp b/include/graaflib/algorithm/graph_traversal.tpp index 3fad0e65..fbf513f5 100644 --- a/include/graaflib/algorithm/graph_traversal.tpp +++ b/include/graaflib/algorithm/graph_traversal.tpp @@ -8,10 +8,46 @@ namespace graaf::algorithm { namespace detail { -template -void do_bfs(const graph& graph, - std::unordered_set& seen_vertices, - vertex_id_t start_vertex, const CALLBACK_T& callback) { +template +bool do_dfs(const graph& graph, + std::unordered_set& seen_vertices, vertex_id_t current, + const EDGE_CALLBACK_T& edge_callback, + const SEARCH_TERMINATION_STRATEGY_T& search_termination_strategy) { + seen_vertices.insert(current); + + if (search_termination_strategy(current)) { + return false; + } + + for (auto neighbor_vertex : graph.get_neighbors(current)) { + if (!seen_vertices.contains(neighbor_vertex)) { + edge_callback(edge_id_t{current, neighbor_vertex}); + if (!do_dfs(graph, seen_vertices, neighbor_vertex, edge_callback, + search_termination_strategy)) { + // Further down the call stack we have hit the search termination point. + // Bubble this up the call stack. + return false; + } + } + } + + // We did not hit the search termination point + return true; +} + +} // namespace detail + +template + requires std::invocable && + std::is_invocable_r_v +void breadth_first_traverse( + const graph& graph, vertex_id_t start_vertex, + const EDGE_CALLBACK_T& edge_callback, + const SEARCH_TERMINATION_STRATEGY_T& search_termination_strategy) { + std::unordered_set seen_vertices{}; std::queue to_explore{}; to_explore.push(start_vertex); @@ -20,47 +56,32 @@ void do_bfs(const graph& graph, const auto current{to_explore.front()}; to_explore.pop(); + if (search_termination_strategy(current)) { + return; + } + seen_vertices.insert(current); for (const auto neighbor_vertex : graph.get_neighbors(current)) { if (!seen_vertices.contains(neighbor_vertex)) { - callback(edge_id_t{current, neighbor_vertex}); + edge_callback(edge_id_t{current, neighbor_vertex}); to_explore.push(neighbor_vertex); } } } } -template -void do_dfs(const graph& graph, - std::unordered_set& seen_vertices, vertex_id_t current, - const CALLBACK_T& callback) { - seen_vertices.insert(current); - - for (auto neighbor_vertex : graph.get_neighbors(current)) { - if (!seen_vertices.contains(neighbor_vertex)) { - callback(edge_id_t{current, neighbor_vertex}); - do_dfs(graph, seen_vertices, neighbor_vertex, callback); - } - } -} - -} // namespace detail - -template - requires std::invocable -void breadth_first_traverse(const graph& graph, - vertex_id_t start_vertex, - const CALLBACK_T& callback) { - std::unordered_set seen_vertices{}; - return detail::do_bfs(graph, seen_vertices, start_vertex, callback); -} - -template - requires std::invocable -void depth_first_traverse(const graph& graph, vertex_id_t start_vertex, - const CALLBACK_T& callback) { +template + requires std::invocable && + std::is_invocable_r_v +void depth_first_traverse( + const graph& graph, vertex_id_t start_vertex, + const EDGE_CALLBACK_T& edge_callback, + const SEARCH_TERMINATION_STRATEGY_T& search_termination_strategy) { std::unordered_set seen_vertices{}; - return detail::do_dfs(graph, seen_vertices, start_vertex, callback); + detail::do_dfs(graph, seen_vertices, start_vertex, edge_callback, + search_termination_strategy); } } // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path.tpp b/include/graaflib/algorithm/shortest_path.tpp index ea26ff81..6fbdcb85 100644 --- a/include/graaflib/algorithm/shortest_path.tpp +++ b/include/graaflib/algorithm/shortest_path.tpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -48,28 +50,26 @@ template std::optional> bfs_shortest_path( const graph& graph, vertex_id_t start_vertex, vertex_id_t end_vertex) { - std::unordered_map> vertex_info; - std::queue to_explore{}; + std::unordered_map> vertex_info{ + {start_vertex, {start_vertex, 0, start_vertex}}}; - vertex_info[start_vertex] = {start_vertex, 1, start_vertex}; - to_explore.push(start_vertex); + const auto callback{[&vertex_info](const edge_id_t& edge) { + const auto [source, target]{edge}; - while (!to_explore.empty()) { - auto current{to_explore.front()}; - to_explore.pop(); - - if (current == end_vertex) { - break; + if (!vertex_info.contains(target)) { + vertex_info[target] = {target, vertex_info[source].dist_from_start + 1, + source}; } + }}; - for (const auto neighbor : graph.get_neighbors(current)) { - if (!vertex_info.contains(neighbor)) { - vertex_info[neighbor] = { - neighbor, vertex_info[current].dist_from_start + 1, current}; - to_explore.push(neighbor); - } - } - } + // We keep searching until we have reached the target vertex + const auto search_termination_strategy{ + [end_vertex](const vertex_id_t vertex_id) { + return vertex_id == end_vertex; + }}; + + breadth_first_traverse(graph, start_vertex, callback, + search_termination_strategy); return reconstruct_path(start_vertex, end_vertex, vertex_info); } diff --git a/test/graaflib/algorithm/graph_traversal_test.cpp b/test/graaflib/algorithm/graph_traversal_test.cpp index b87df53d..09fdaa3f 100644 --- a/test/graaflib/algorithm/graph_traversal_test.cpp +++ b/test/graaflib/algorithm/graph_traversal_test.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace graaf::algorithm { @@ -40,6 +41,39 @@ struct record_edge_callback { } }; +template +struct scenario { + GRAPH_T graph{}; + + // This vector decouples the test scenario from the graphs internal id + // generation logic. The first element in this vector is the first vertex and + // so on... + std::vector vertex_ids{}; +}; + +template +[[nodiscard]] scenario create_complex_scenario() { + std::vector vertex_ids{}; + vertex_ids.reserve(5); + + GRAPH_T graph{}; + + vertex_ids.push_back(graph.add_vertex(10)); + vertex_ids.push_back(graph.add_vertex(20)); + vertex_ids.push_back(graph.add_vertex(30)); + vertex_ids.push_back(graph.add_vertex(40)); + vertex_ids.push_back(graph.add_vertex(50)); + + // All edges are in the search direction, so the graph specialization does not + // matter + graph.add_edge(vertex_ids[0], vertex_ids[1], 100); + graph.add_edge(vertex_ids[0], vertex_ids[2], 200); + graph.add_edge(vertex_ids[2], vertex_ids[3], 300); + graph.add_edge(vertex_ids[2], vertex_ids[4], 400); + + return {std::move(graph), std::move(vertex_ids)}; +} + } // namespace TYPED_TEST(TypedGraphTraversalTest, MinimalGraphDFS) { @@ -172,85 +206,185 @@ TEST(GraphTraversalTest, DirectedGraphEdgeWrongDirectionBFS) { TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFS) { // GIVEN using graph_t = typename TestFixture::graph_t; - graph_t graph{}; - - const auto vertex_1{graph.add_vertex(10)}; - const auto vertex_2{graph.add_vertex(20)}; - const auto vertex_3{graph.add_vertex(30)}; - const auto vertex_4{graph.add_vertex(40)}; - const auto vertex_5{graph.add_vertex(50)}; - - // All edges are in the search direction, so the graph specialization does not - // matter - graph.add_edge(vertex_1, vertex_2, 100); - graph.add_edge(vertex_1, vertex_3, 200); - graph.add_edge(vertex_3, vertex_4, 300); - graph.add_edge(vertex_3, vertex_5, 400); + const auto [graph, vertex_ids]{create_complex_scenario()}; seen_edges_t seen_edges{}; edge_order_t edge_order{}; // WHEN - depth_first_traverse(graph, vertex_1, + depth_first_traverse(graph, vertex_ids[0], record_edge_callback{seen_edges, edge_order}); // THEN - const seen_edges_t expected_edges{{vertex_1, vertex_2}, - {vertex_1, vertex_3}, - {vertex_3, vertex_4}, - {vertex_3, vertex_5}}; + const seen_edges_t expected_edges{{vertex_ids[0], vertex_ids[1]}, + {vertex_ids[0], vertex_ids[2]}, + {vertex_ids[2], vertex_ids[3]}, + {vertex_ids[2], vertex_ids[4]}}; ASSERT_EQ(seen_edges, expected_edges); // We do DFS, so while the ordering between neighbors is undefined, // the order within one branch should be preserved - ASSERT_TRUE(edge_order.at({vertex_3, vertex_4}) > - edge_order.at({vertex_1, vertex_3})); - ASSERT_TRUE(edge_order.at({vertex_3, vertex_5}) > - edge_order.at({vertex_1, vertex_3})); + ASSERT_TRUE(edge_order.at({vertex_ids[2], vertex_ids[3]}) > + edge_order.at({vertex_ids[0], vertex_ids[2]})); + ASSERT_TRUE(edge_order.at({vertex_ids[2], vertex_ids[4]}) > + edge_order.at({vertex_ids[0], vertex_ids[2]})); } TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphBFS) { // GIVEN using graph_t = typename TestFixture::graph_t; - graph_t graph{}; - - const auto vertex_1{graph.add_vertex(10)}; - const auto vertex_2{graph.add_vertex(20)}; - const auto vertex_3{graph.add_vertex(30)}; - const auto vertex_4{graph.add_vertex(40)}; - const auto vertex_5{graph.add_vertex(50)}; - - // All edges are in the search direction, so the graph specialization does not - // matter - graph.add_edge(vertex_1, vertex_2, 100); - graph.add_edge(vertex_1, vertex_3, 200); - graph.add_edge(vertex_3, vertex_4, 300); - graph.add_edge(vertex_3, vertex_5, 400); + const auto [graph, vertex_ids]{create_complex_scenario()}; seen_edges_t seen_edges{}; edge_order_t edge_order{}; // WHEN - breadth_first_traverse(graph, vertex_1, + breadth_first_traverse(graph, vertex_ids[0], record_edge_callback{seen_edges, edge_order}); // THEN - const seen_edges_t expected_edges{{vertex_1, vertex_2}, - {vertex_1, vertex_3}, - {vertex_3, vertex_4}, - {vertex_3, vertex_5}}; + const seen_edges_t expected_edges{{vertex_ids[0], vertex_ids[1]}, + {vertex_ids[0], vertex_ids[2]}, + {vertex_ids[2], vertex_ids[3]}, + {vertex_ids[2], vertex_ids[4]}}; ASSERT_EQ(seen_edges, expected_edges); // We do BFS, so all immediate neighbors must be traversed first before going // deeper in the graph - ASSERT_TRUE(edge_order.at({vertex_1, vertex_2}) < - edge_order.at({vertex_3, vertex_4}) && - edge_order.at({vertex_1, vertex_2}) < - edge_order.at({vertex_3, vertex_5})); - ASSERT_TRUE(edge_order.at({vertex_1, vertex_3}) < - edge_order.at({vertex_3, vertex_4}) && - edge_order.at({vertex_1, vertex_3}) < - edge_order.at({vertex_3, vertex_5})); + ASSERT_TRUE(edge_order.at({vertex_ids[0], vertex_ids[1]}) < + edge_order.at({vertex_ids[2], vertex_ids[3]}) && + edge_order.at({vertex_ids[0], vertex_ids[1]}) < + edge_order.at({vertex_ids[2], vertex_ids[4]})); + ASSERT_TRUE(edge_order.at({vertex_ids[0], vertex_ids[2]}) < + edge_order.at({vertex_ids[2], vertex_ids[3]}) && + edge_order.at({vertex_ids[0], vertex_ids[2]}) < + edge_order.at({vertex_ids[2], vertex_ids[4]})); +} + +TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSImmediateTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, vertex_ids]{create_complex_scenario()}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // Always returns true such that the search immediately terminates + const auto immediate_termination_strategy{ + [](const vertex_id_t& /*vertex*/) { return true; }}; + + // WHEN + depth_first_traverse(graph, vertex_ids[0], + record_edge_callback{seen_edges, edge_order}, + immediate_termination_strategy); + + // THEN + const seen_edges_t expected_edges{}; + ASSERT_EQ(seen_edges, expected_edges); +} + +TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphBFSImmediateTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, vertex_ids]{create_complex_scenario()}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // Always returns true such that the search immediately terminates + const auto immediate_termination_strategy{ + [](const vertex_id_t& /*vertex*/) { return true; }}; + + // WHEN + breadth_first_traverse(graph, vertex_ids[0], + record_edge_callback{seen_edges, edge_order}, + immediate_termination_strategy); + + // THEN + const seen_edges_t expected_edges{}; + ASSERT_EQ(seen_edges, expected_edges); +} + +TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, vertex_ids]{create_complex_scenario()}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + const auto termination_strategy{ + [target = vertex_ids[2]](const vertex_id_t& vertex) { + return vertex == target; + }}; + + // WHEN + breadth_first_traverse(graph, vertex_ids[0], + record_edge_callback{seen_edges, edge_order}, + termination_strategy); + + // THEN - Since there is no clear iteration order between the neighbors of the + // 0-th vertex, there are two options for which edges we traversed + const seen_edges_t expected_edges_option_1{{vertex_ids[0], vertex_ids[1]}, + {vertex_ids[0], vertex_ids[2]}}; + const seen_edges_t expected_edges_option_2{{vertex_ids[0], vertex_ids[2]}}; + ASSERT_TRUE(seen_edges == expected_edges_option_1 || + seen_edges == expected_edges_option_2); +} + +TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSLaterTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, vertex_ids]{create_complex_scenario()}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // Here we terminate deeper in the tree + const auto termination_strategy{ + [target = vertex_ids[3]](const vertex_id_t& vertex) { + return vertex == target; + }}; + + // WHEN + breadth_first_traverse(graph, vertex_ids[0], + record_edge_callback{seen_edges, edge_order}, + termination_strategy); + + // THEN - Since there is no clear iteration order between the neighbors of the + // 0-th vertex, but we must have AT_LEAST seen the following edges + const seen_edges_t expected_edges{{vertex_ids[0], vertex_ids[2]}, + {vertex_ids[2], vertex_ids[3]}}; + for (const auto& expected_edge : expected_edges) { + ASSERT_TRUE(seen_edges.contains(expected_edge)); + } +} + +TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphBFSTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, vertex_ids]{create_complex_scenario()}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + const auto termination_strategy{ + [target = vertex_ids[2]](const vertex_id_t& vertex) { + return vertex == target; + }}; + + // WHEN + breadth_first_traverse(graph, vertex_ids[0], + record_edge_callback{seen_edges, edge_order}, + termination_strategy); + + // THEN - Since there is no clear iteration order between the neighbors of the + // 0-th vertex, there are two options for which edges we traversed + const seen_edges_t expected_edges_option_1{{vertex_ids[0], vertex_ids[1]}, + {vertex_ids[0], vertex_ids[2]}}; + const seen_edges_t expected_edges_option_2{{vertex_ids[0], vertex_ids[2]}}; + ASSERT_TRUE(seen_edges == expected_edges_option_1 || + seen_edges == expected_edges_option_2); } TEST(GraphTraversalTest, MoreComplexDirectedGraphEdgeWrongDirectionDFS) { diff --git a/test/graaflib/algorithm/shortest_path_test.cpp b/test/graaflib/algorithm/shortest_path_test.cpp index f4575cca..be96aa41 100644 --- a/test/graaflib/algorithm/shortest_path_test.cpp +++ b/test/graaflib/algorithm/shortest_path_test.cpp @@ -27,7 +27,7 @@ TYPED_TEST(TypedShortestPathTest, BfsMinimalShortestPath) { const auto path = bfs_shortest_path(graph, vertex_1, vertex_1); // THEN - const graph_path expected_path{{vertex_1}, 1}; + const graph_path expected_path{{vertex_1}, 0}; ASSERT_EQ(path, expected_path); } @@ -62,7 +62,7 @@ TYPED_TEST(TypedShortestPathTest, BfsSimpleShortestPath) { const auto path = bfs_shortest_path(graph, vertex_1, vertex_2); // THEN - const graph_path expected_path{{vertex_1, vertex_2}, 2}; + const graph_path expected_path{{vertex_1, vertex_2}, 1}; ASSERT_EQ(path, expected_path); } @@ -90,7 +90,7 @@ TYPED_TEST(TypedShortestPathTest, BfsMoreComplexShortestPath) { const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); // THEN - const graph_path expected_path{{vertex_1, vertex_3, vertex_5}, 3}; + const graph_path expected_path{{vertex_1, vertex_3, vertex_5}, 2}; ASSERT_EQ(path, expected_path); } @@ -118,7 +118,7 @@ TYPED_TEST(TypedShortestPathTest, BfsCyclicShortestPath) { // THEN const graph_path expected_path{{vertex_1, vertex_2, vertex_3, vertex_5}, - 4}; + 3}; ASSERT_EQ(path, expected_path); } @@ -144,7 +144,7 @@ TEST(ShortestPathTest, BfsDirectedrWrongDirectionShortestPath) { // THEN const graph_path expected_path{ - {vertex_1, vertex_2, vertex_4, vertex_3, vertex_5}, 5}; + {vertex_1, vertex_2, vertex_4, vertex_3, vertex_5}, 4}; ASSERT_EQ(path, expected_path); }