diff --git a/examples/shortest_path/main.cpp b/examples/shortest_path/main.cpp index c6ca0119..ed4fa86b 100644 --- a/examples/shortest_path/main.cpp +++ b/examples/shortest_path/main.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include diff --git a/examples/transport/main.cpp b/examples/transport/main.cpp index aa2c9b41..dc2fdfa6 100644 --- a/examples/transport/main.cpp +++ b/examples/transport/main.cpp @@ -1,10 +1,13 @@ #include -#include -#include +#include +#include +#include +#include #include #include #include +#include #include struct station { diff --git a/include/graaflib/algorithm/cycle_detection.h b/include/graaflib/algorithm/cycle_detection/dfs_cycle_detection.h similarity index 94% rename from include/graaflib/algorithm/cycle_detection.h rename to include/graaflib/algorithm/cycle_detection/dfs_cycle_detection.h index 1c40e981..65ccc525 100644 --- a/include/graaflib/algorithm/cycle_detection.h +++ b/include/graaflib/algorithm/cycle_detection/dfs_cycle_detection.h @@ -24,4 +24,4 @@ template } // namespace graaf::algorithm -#include "cycle_detection.tpp" \ No newline at end of file +#include "dfs_cycle_detection.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/cycle_detection.tpp b/include/graaflib/algorithm/cycle_detection/dfs_cycle_detection.tpp similarity index 100% rename from include/graaflib/algorithm/cycle_detection.tpp rename to include/graaflib/algorithm/cycle_detection/dfs_cycle_detection.tpp diff --git a/include/graaflib/algorithm/graph_traversal.h b/include/graaflib/algorithm/graph_traversal.h deleted file mode 100644 index 6229392a..00000000 --- a/include/graaflib/algorithm/graph_traversal.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include -#include - -#include - -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 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 < - 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 - * reachable vertices in a DFS manner. - * - * @param graph The graph to traverse. - * @param start_vertex Vertex id where the traversal should be started. - * @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 < - 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 - -#include "graph_traversal.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/graph_traversal/breadth_first_search.h b/include/graaflib/algorithm/graph_traversal/breadth_first_search.h new file mode 100644 index 00000000..abb9279a --- /dev/null +++ b/include/graaflib/algorithm/graph_traversal/breadth_first_search.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include + +namespace graaf::algorithm { + +/** + * @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 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 < + 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{}); + +} // namespace graaf::algorithm + +#include "breadth_first_search.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/graph_traversal/breadth_first_search.tpp b/include/graaflib/algorithm/graph_traversal/breadth_first_search.tpp new file mode 100644 index 00000000..4ad42114 --- /dev/null +++ b/include/graaflib/algorithm/graph_traversal/breadth_first_search.tpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +namespace graaf::algorithm { + +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); + + while (!to_explore.empty()) { + 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)) { + edge_callback(edge_id_t{current, neighbor_vertex}); + to_explore.push(neighbor_vertex); + } + } + } +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/graph_traversal/common.h b/include/graaflib/algorithm/graph_traversal/common.h new file mode 100644 index 00000000..2fd42c75 --- /dev/null +++ b/include/graaflib/algorithm/graph_traversal/common.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +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 + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/graph_traversal/depth_first_search.h b/include/graaflib/algorithm/graph_traversal/depth_first_search.h new file mode 100644 index 00000000..8224e907 --- /dev/null +++ b/include/graaflib/algorithm/graph_traversal/depth_first_search.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include + +namespace graaf::algorithm { + +/** + * @brief Traverses the graph, starting at start_vertex, and visits all + * reachable vertices in a DFS manner. + * + * @param graph The graph to traverse. + * @param start_vertex Vertex id where the traversal should be started. + * @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 < + 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 + +#include "depth_first_search.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/graph_traversal.tpp b/include/graaflib/algorithm/graph_traversal/depth_first_search.tpp similarity index 63% rename from include/graaflib/algorithm/graph_traversal.tpp rename to include/graaflib/algorithm/graph_traversal/depth_first_search.tpp index fbf513f5..db01b625 100644 --- a/include/graaflib/algorithm/graph_traversal.tpp +++ b/include/graaflib/algorithm/graph_traversal/depth_first_search.tpp @@ -38,38 +38,6 @@ bool do_dfs(const graph& graph, } // 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); - - while (!to_explore.empty()) { - 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)) { - edge_callback(edge_id_t{current, neighbor_vertex}); - to_explore.push(neighbor_vertex); - } - } - } -} - template requires std::invocable && diff --git a/include/graaflib/algorithm/minimum_spanning_tree/kruskal.h b/include/graaflib/algorithm/minimum_spanning_tree/kruskal.h new file mode 100644 index 00000000..b67c5b98 --- /dev/null +++ b/include/graaflib/algorithm/minimum_spanning_tree/kruskal.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +namespace graaf::algorithm { +/** + * Computes the minimum spanning tree (MST) or minimum spanning forest of a + * graph using Kruskal's algorithm. + * + * @tparam V The vertex type of the graph. + * @tparam E The edge type of the graph. + * @param graph The input graph. + * @return A vector of edges forming the MST or minimum spanning forest. + */ +template +[[nodiscard]] std::vector kruskal_minimum_spanning_tree( + const graph& graph); + +} // namespace graaf::algorithm + +#include "kruskal.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/minimum_spanning_tree.tpp b/include/graaflib/algorithm/minimum_spanning_tree/kruskal.tpp similarity index 67% rename from include/graaflib/algorithm/minimum_spanning_tree.tpp rename to include/graaflib/algorithm/minimum_spanning_tree/kruskal.tpp index ad79a1e4..e0ff7361 100644 --- a/include/graaflib/algorithm/minimum_spanning_tree.tpp +++ b/include/graaflib/algorithm/minimum_spanning_tree/kruskal.tpp @@ -61,23 +61,6 @@ struct edge_to_process : public weighted_edge { } }; -template -[[nodiscard]] std::vector find_candidate_edges( - const GRAPH_T& graph, - const std::unordered_set& fringe_vertices) { - std::vector candidates{}; - - for (const auto fringe_vertex : fringe_vertices) { - for (const auto neighbor : graph.get_neighbors(fringe_vertex)) { - if (!fringe_vertices.contains(neighbor)) { - candidates.emplace_back(fringe_vertex, neighbor); - } - } - } - - return candidates; -} - }; // namespace detail template @@ -115,36 +98,4 @@ std::vector kruskal_minimum_spanning_tree( return mst_edges; } -template -std::optional> prim_minimum_spanning_tree( - const graph& graph, - vertex_id_t start_vertex) { - std::vector edges_in_mst{}; - edges_in_mst.reserve( - graph.edge_count()); // Reserve the upper bound of edges in the mst - - std::unordered_set fringe_vertices{start_vertex}; - - while (fringe_vertices.size() < graph.vertex_count()) { - const auto candidates{detail::find_candidate_edges(graph, fringe_vertices)}; - - if (candidates.empty()) { - // The graph is not connected - return std::nullopt; - } - - const edge_id_t mst_edge{*std::ranges::min_element( - candidates, - [graph](const edge_id_t& lhs, const edge_id_t& rhs) -> bool { - return get_weight(graph.get_edge(lhs)) < - get_weight(graph.get_edge(rhs)); - })}; - - edges_in_mst.emplace_back(mst_edge); - fringe_vertices.insert(mst_edge.second); - } - - return edges_in_mst; -} - }; // namespace graaf::algorithm diff --git a/include/graaflib/algorithm/minimum_spanning_tree.h b/include/graaflib/algorithm/minimum_spanning_tree/prim.h similarity index 62% rename from include/graaflib/algorithm/minimum_spanning_tree.h rename to include/graaflib/algorithm/minimum_spanning_tree/prim.h index d12fb67b..820c2548 100644 --- a/include/graaflib/algorithm/minimum_spanning_tree.h +++ b/include/graaflib/algorithm/minimum_spanning_tree/prim.h @@ -7,18 +7,6 @@ #include namespace graaf::algorithm { -/** - * Computes the minimum spanning tree (MST) or minimum spanning forest of a - * graph using Kruskal's algorithm. - * - * @tparam V The vertex type of the graph. - * @tparam E The edge type of the graph. - * @param graph The input graph. - * @return A vector of edges forming the MST or minimum spanning forest. - */ -template -[[nodiscard]] std::vector kruskal_minimum_spanning_tree( - const graph& graph); /** * Computes the minimum spanning tree (MST) of a graph using Prim's algorithm. @@ -37,4 +25,4 @@ template } // namespace graaf::algorithm -#include "minimum_spanning_tree.tpp" \ No newline at end of file +#include "prim.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/minimum_spanning_tree/prim.tpp b/include/graaflib/algorithm/minimum_spanning_tree/prim.tpp new file mode 100644 index 00000000..59de63b4 --- /dev/null +++ b/include/graaflib/algorithm/minimum_spanning_tree/prim.tpp @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include +#include +#include + +namespace graaf::algorithm { + +namespace detail { + +template +[[nodiscard]] std::vector find_candidate_edges( + const GRAPH_T& graph, + const std::unordered_set& fringe_vertices) { + std::vector candidates{}; + + for (const auto fringe_vertex : fringe_vertices) { + for (const auto neighbor : graph.get_neighbors(fringe_vertex)) { + if (!fringe_vertices.contains(neighbor)) { + candidates.emplace_back(fringe_vertex, neighbor); + } + } + } + + return candidates; +} + +}; // namespace detail + +template +std::optional> prim_minimum_spanning_tree( + const graph& graph, + vertex_id_t start_vertex) { + std::vector edges_in_mst{}; + edges_in_mst.reserve( + graph.edge_count()); // Reserve the upper bound of edges in the mst + + std::unordered_set fringe_vertices{start_vertex}; + + while (fringe_vertices.size() < graph.vertex_count()) { + const auto candidates{detail::find_candidate_edges(graph, fringe_vertices)}; + + if (candidates.empty()) { + // The graph is not connected + return std::nullopt; + } + + const edge_id_t mst_edge{*std::ranges::min_element( + candidates, + [graph](const edge_id_t& lhs, const edge_id_t& rhs) -> bool { + return get_weight(graph.get_edge(lhs)) < + get_weight(graph.get_edge(rhs)); + })}; + + edges_in_mst.emplace_back(mst_edge); + fringe_vertices.insert(mst_edge.second); + } + + return edges_in_mst; +} + +}; // namespace graaf::algorithm diff --git a/include/graaflib/algorithm/shortest_path.h b/include/graaflib/algorithm/shortest_path.h deleted file mode 100644 index 73a47e61..00000000 --- a/include/graaflib/algorithm/shortest_path.h +++ /dev/null @@ -1,119 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -namespace graaf::algorithm { - -template -struct graph_path { - std::list vertices; - WEIGHT_T total_weight; - - bool operator==(const graph_path& other) const { - return vertices == other.vertices && total_weight == other.total_weight; - } -}; - -/** - * @brief calculates the shortest path between one start_vertex and one - * end_vertex using BFS. This does not consider edge weights. - * - * @param graph The graph to extract shortest path from. - * @param start_vertex Vertex id where the shortest path should start. - * @param end_vertex Vertex id where the shortest path should end. - * @return An optional with the shortest path (list of vertices) if found. - */ -template ()))> -std::optional> bfs_shortest_path( - const graph& graph, vertex_id_t start_vertex, - vertex_id_t end_vertex); - -/** - * @brief calculates the shortest path between one start_vertex and one - * end_vertex using Dijkstra's algorithm. Works on both weighted as well as - * unweighted graphs. For unweighted graphs, a unit weight is used for each - * edge. - * - * @param graph The graph to extract shortest path from. - * @param start_vertex Vertex id where the shortest path should start. - * @param end_vertex Vertex id where the shortest path should end. - * @return An optional with the shortest path (list of vertices) if found. - */ -template ()))> -std::optional> dijkstra_shortest_path( - const graph& graph, vertex_id_t start_vertex, - vertex_id_t end_vertex); - -/** - * Find the shortest paths from a source vertex to all other vertices in the - * graph using Dijkstra's algorithm. - * - * @tparam V The vertex type of the graph. - * @tparam E The edge type of the graph. - * @tparam T The graph type (directed or undirected). - * @tparam WEIGHT_T The type of edge weights. - * @param graph The graph we want to search. - * @param source_vertex The source vertex from which to compute shortest paths. - * @return A map containing the shortest paths from the source vertex to all - * other vertices. The map keys are target vertex IDs, and the values are - * instances of graph_path, representing the shortest distance and the path - * (list of vertex IDs) from the source to the target. If a vertex is not - * reachable from the source, its entry will be absent from the map. - */ -template ()))> -[[nodiscard]] std::unordered_map> -dijkstra_shortest_paths(const graph& graph, vertex_id_t source_vertex); - -/** - * Find the shortest paths from a source vertex to all other vertices using - * the Bellman-Ford algorithm. - * - * @tparam V The vertex type of the graph. - * @tparam E The edge type of the graph. - * @tparam T The graph specialization (directed or undirected). - * @tparam WEIGHT_T The type of weight associated with the edges. - * @param graph The graph in which to find the shortest paths. - * @param start_vertex The source vertex for the shortest paths. - * @return A map of target vertex IDs to shortest path structures. Each - * value contains a graph_path object representing the - * shortest path from the source vertex to the respective vertex. - * If a vertex is unreachable from the source, its entry will be - * absent from the map. - */ -template ()))> -std::unordered_map> -bellman_ford_shortest_paths(const graph& graph, - vertex_id_t start_vertex); - -/** - * @brief Finds the shortest path between a start_vertex and target_vertex - * using the A* search algorithm. - * - * @param graph The graph to search in. - * @param start_vertex The starting vertex for the search. - * @param target_vertex The target vertex to reach. - * @param heuristic A heuristic function estimating the cost from a vertex to - * the target. - * @return An optional containing the shortest path if found, or std::nullopt if - * no path exists. - */ -template ()))> - requires std::is_invocable_r_v -std::optional> a_star_search(const graph& graph, - vertex_id_t start_vertex, - vertex_id_t target_vertex, - const HEURISTIC_T& heuristic); - -} // namespace graaf::algorithm - -#include "shortest_path.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path.tpp b/include/graaflib/algorithm/shortest_path.tpp deleted file mode 100644 index 4eefe946..00000000 --- a/include/graaflib/algorithm/shortest_path.tpp +++ /dev/null @@ -1,319 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -namespace graaf::algorithm { - -namespace detail { - -template -struct path_vertex { - vertex_id_t id; - WEIGHT_T dist_from_start; - vertex_id_t prev_id; - - [[nodiscard]] bool operator>(const path_vertex& other) { - return dist_from_start > other.dist_from_start; - } -}; - -template -std::optional> reconstruct_path( - vertex_id_t start_vertex, vertex_id_t end_vertex, - std::unordered_map>& vertex_info) { - if (!vertex_info.contains(end_vertex)) { - return std::nullopt; - } - - graph_path path; - auto current = end_vertex; - - while (current != start_vertex) { - path.vertices.push_front(current); - current = vertex_info[current].prev_id; - } - - path.vertices.push_front(start_vertex); - path.total_weight = vertex_info[end_vertex].dist_from_start; - return path; -} - -} // namespace detail - -template -std::optional> bfs_shortest_path( - const graph& graph, vertex_id_t start_vertex, - vertex_id_t end_vertex) { - std::unordered_map> vertex_info{ - {start_vertex, {start_vertex, 0, start_vertex}}}; - - const auto callback{[&vertex_info](const edge_id_t& edge) { - const auto [source, target]{edge}; - - if (!vertex_info.contains(target)) { - vertex_info[target] = {target, vertex_info[source].dist_from_start + 1, - source}; - } - }}; - - // 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); -} - -template -std::optional> dijkstra_shortest_path( - const graph& graph, vertex_id_t start_vertex, - vertex_id_t end_vertex) { - using weighted_path_item = detail::path_vertex; - using dijkstra_queue_t = - std::priority_queue, - std::greater<>>; - dijkstra_queue_t to_explore{}; - std::unordered_map vertex_info; - - vertex_info[start_vertex] = {start_vertex, 0, start_vertex}; - to_explore.push(vertex_info[start_vertex]); - - while (!to_explore.empty()) { - auto current{to_explore.top()}; - to_explore.pop(); - - if (current.id == end_vertex) { - break; - } - - for (const auto& neighbor : graph.get_neighbors(current.id)) { - WEIGHT_T edge_weight = get_weight(graph.get_edge(current.id, neighbor)); - - if (edge_weight < 0) { - std::ostringstream error_msg; - error_msg << "Negative edge weight [" << edge_weight - << "] between vertices [" << current.id << "] -> [" - << neighbor << "]."; - throw std::invalid_argument{error_msg.str()}; - } - - WEIGHT_T distance = current.dist_from_start + edge_weight; - - if (!vertex_info.contains(neighbor) || - distance < vertex_info[neighbor].dist_from_start) { - vertex_info[neighbor] = {neighbor, distance, current.id}; - to_explore.push(vertex_info[neighbor]); - } - } - } - - return reconstruct_path(start_vertex, end_vertex, vertex_info); -} - -template -[[nodiscard]] std::unordered_map> -dijkstra_shortest_paths(const graph& graph, - vertex_id_t source_vertex) { - std::unordered_map> shortest_paths; - - using weighted_path_item = detail::path_vertex; - using dijkstra_queue_t = - std::priority_queue, - std::greater<>>; - dijkstra_queue_t to_explore{}; - - shortest_paths[source_vertex].total_weight = 0; - shortest_paths[source_vertex].vertices.push_back(source_vertex); - to_explore.push(weighted_path_item{source_vertex, 0}); - - while (!to_explore.empty()) { - auto current{to_explore.top()}; - to_explore.pop(); - - if (shortest_paths.contains(current.id) && - current.dist_from_start > shortest_paths[current.id].total_weight) { - continue; - } - - for (const auto neighbor : graph.get_neighbors(current.id)) { - WEIGHT_T edge_weight = get_weight(graph.get_edge(current.id, neighbor)); - - if (edge_weight < 0) { - std::ostringstream error_msg; - error_msg << "Negative edge weight [" << edge_weight - << "] between vertices [" << current.id << "] -> [" - << neighbor << "]."; - throw std::invalid_argument{error_msg.str()}; - } - - WEIGHT_T distance = current.dist_from_start + edge_weight; - - if (!shortest_paths.contains(neighbor) || - distance < shortest_paths[neighbor].total_weight) { - shortest_paths[neighbor].total_weight = distance; - shortest_paths[neighbor].vertices = shortest_paths[current.id].vertices; - shortest_paths[neighbor].vertices.push_back(neighbor); - to_explore.push(weighted_path_item{neighbor, distance}); - } - } - } - - return shortest_paths; -} - -template -std::unordered_map> -bellman_ford_shortest_paths(const graph& graph, - vertex_id_t start_vertex) { - std::unordered_map> shortest_paths; - - const auto found_shorter_path{ - [&shortest_paths](edge_id_t edge_id, const E& edge) { - const auto weight = get_weight(edge); - const auto [u, v]{edge_id}; - return shortest_paths[u].total_weight != - std::numeric_limits::max() && - shortest_paths[u].total_weight + weight < - shortest_paths[v].total_weight; - }}; - - // Initialize the shortest path distances from the starting vertex to - // "infinity" for all vertices - for (const auto& [vertex_id, _] : graph.get_vertices()) { - shortest_paths[vertex_id] = {{vertex_id}, - std::numeric_limits::max()}; - } - - // Set the distance from the starting vertex to itself to 0 - shortest_paths[start_vertex] = {{start_vertex}, 0}; - - // Relax edges for |V| - 1 iterations - for (std::size_t i = 1; i < graph.vertex_count(); ++i) { - for (const auto& [edge_id, edge] : graph.get_edges()) { - const auto [u, v]{edge_id}; - WEIGHT_T weight = get_weight(edge); - - if (found_shorter_path(edge_id, edge)) { - // Update the shortest path to vertex v - shortest_paths[v] = { - {shortest_paths[u].vertices}, - shortest_paths[u].total_weight + weight, - }; - shortest_paths[v].vertices.push_back(v); - } - } - } - // Negative cycle detection by doing an additional pass in the graph - for (const auto& [edge_id, edge] : graph.get_edges()) { - const auto [u, v]{edge_id}; - WEIGHT_T weight = get_weight(edge); - if (found_shorter_path(edge_id, edge)) { - throw std::invalid_argument{"Negative cycle detected in the graph."}; - } - } - return shortest_paths; -} - -template - requires std::is_invocable_r_v -std::optional> a_star_search( - const graph& graph, vertex_id_t start_vertex, - vertex_id_t target_vertex, const HEURISTIC_T& heuristic) { - // Define a priority queue for open set of vertices to explore. - // This part is similar to dijkstra_shortest_path - using weighted_path_item = detail::path_vertex; - // The set of discovered vertices that may need to be (re-)expanded. - // f_score represents the estimated total cost of the path from the start - // vertex to the goal vertex through the current vertex. - // It's a combination of g_score and h_score: - // f_score[n] = g_score[n] + h_score[n] - // For vertex n, prev_id in path_vertex is the vertex immediately preceding - // it on the cheapest path from the start to n currently known. The priority - // queue uses internally a binary heap. To get the minimum element, we use - // the std::greater comparator. - using a_star_queue_t = - std::priority_queue, - std::greater<>>; - a_star_queue_t open_set{}; - - // For vertex n, g_score[n] is the cost of the cheapest path from start to n - // currently known. It tracks the cost of reaching each vertex - std::unordered_map g_score; - // Initialize g_score map. - g_score[start_vertex] = 0; - - std::unordered_map vertex_info; - vertex_info[start_vertex] = { - start_vertex, - heuristic(start_vertex), // f_score[n] = g_score[n] + h(n), and - // g_score[n] is 0 if n is start_vertex. - start_vertex}; - - // Initialize start vertex in open set queue - open_set.push(vertex_info[start_vertex]); - - while (!open_set.empty()) { - // Get the vertex with the lowest f_score - auto current{open_set.top()}; - open_set.pop(); - - // Check if current vertex is the target - if (current.id == target_vertex) { - return reconstruct_path(start_vertex, target_vertex, vertex_info); - } - - // Iterate through neighboring vertices - for (const auto& neighbor : graph.get_neighbors(current.id)) { - WEIGHT_T edge_weight = get_weight(graph.get_edge(current.id, neighbor)); - - // A* search does not work on negative edge weights. - if (edge_weight < 0) { - throw std::invalid_argument{fmt::format( - "Negative edge weight [{}] between vertices [{}] -> [{}].", - edge_weight, current.id, neighbor)}; - } - - // tentative_g_score is the distance from start to the neighbor through - // current_vertex - WEIGHT_T tentative_g_score = g_score[current.id] + edge_weight; - - // Checks if vertex_info doesn't contain neighbor yet. - // But if it contains it, and the tentative_g_score is smaller, - // we need to update vertex_info and add it to the open set. - if (!vertex_info.contains(neighbor) || - tentative_g_score < g_score[neighbor]) { - // This path to neighbor is better than any previous one, so we need - // to update our data. Update neighbor's g_score, f_score and previous - // vertex on the path - g_score[neighbor] = tentative_g_score; - auto f_score = tentative_g_score + heuristic(neighbor); - - // always update vertex_info[neighbor] - vertex_info[neighbor] = { - neighbor, // vertex id - f_score, // f_score = tentantive_g_score + h(neighbor) - current.id // neighbor vertex came from current vertex - }; - - open_set.push(vertex_info[neighbor]); - } - } - } - - // No path found - return std::nullopt; -} - -} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/a_star.h b/include/graaflib/algorithm/shortest_path/a_star.h new file mode 100644 index 00000000..fdecc7f2 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/a_star.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace graaf::algorithm { + +/** + * @brief Finds the shortest path between a start_vertex and target_vertex + * using the A* search algorithm. + * + * @param graph The graph to search in. + * @param start_vertex The starting vertex for the search. + * @param target_vertex The target vertex to reach. + * @param heuristic A heuristic function estimating the cost from a vertex to + * the target. + * @return An optional containing the shortest path if found, or std::nullopt if + * no path exists. + */ +template ()))> + requires std::is_invocable_r_v +std::optional> a_star_search(const graph& graph, + vertex_id_t start_vertex, + vertex_id_t target_vertex, + const HEURISTIC_T& heuristic); + +} // namespace graaf::algorithm + +#include "a_star.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/a_star.tpp b/include/graaflib/algorithm/shortest_path/a_star.tpp new file mode 100644 index 00000000..afdf984d --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/a_star.tpp @@ -0,0 +1,98 @@ +#pragma once + +#include + +namespace graaf::algorithm { + +template + requires std::is_invocable_r_v +std::optional> a_star_search( + const graph& graph, vertex_id_t start_vertex, + vertex_id_t target_vertex, const HEURISTIC_T& heuristic) { + // Define a priority queue for open set of vertices to explore. + // This part is similar to dijkstra_shortest_path + using weighted_path_item = detail::path_vertex; + // The set of discovered vertices that may need to be (re-)expanded. + // f_score represents the estimated total cost of the path from the start + // vertex to the goal vertex through the current vertex. + // It's a combination of g_score and h_score: + // f_score[n] = g_score[n] + h_score[n] + // For vertex n, prev_id in path_vertex is the vertex immediately preceding + // it on the cheapest path from the start to n currently known. The priority + // queue uses internally a binary heap. To get the minimum element, we use + // the std::greater comparator. + using a_star_queue_t = + std::priority_queue, + std::greater<>>; + a_star_queue_t open_set{}; + + // For vertex n, g_score[n] is the cost of the cheapest path from start to n + // currently known. It tracks the cost of reaching each vertex + std::unordered_map g_score; + // Initialize g_score map. + g_score[start_vertex] = 0; + + std::unordered_map vertex_info; + vertex_info[start_vertex] = { + start_vertex, + heuristic(start_vertex), // f_score[n] = g_score[n] + h(n), and + // g_score[n] is 0 if n is start_vertex. + start_vertex}; + + // Initialize start vertex in open set queue + open_set.push(vertex_info[start_vertex]); + + while (!open_set.empty()) { + // Get the vertex with the lowest f_score + auto current{open_set.top()}; + open_set.pop(); + + // Check if current vertex is the target + if (current.id == target_vertex) { + return reconstruct_path(start_vertex, target_vertex, vertex_info); + } + + // Iterate through neighboring vertices + for (const auto& neighbor : graph.get_neighbors(current.id)) { + WEIGHT_T edge_weight = get_weight(graph.get_edge(current.id, neighbor)); + + // A* search does not work on negative edge weights. + if (edge_weight < 0) { + throw std::invalid_argument{fmt::format( + "Negative edge weight [{}] between vertices [{}] -> [{}].", + edge_weight, current.id, neighbor)}; + } + + // tentative_g_score is the distance from start to the neighbor through + // current_vertex + WEIGHT_T tentative_g_score = g_score[current.id] + edge_weight; + + // Checks if vertex_info doesn't contain neighbor yet. + // But if it contains it, and the tentative_g_score is smaller, + // we need to update vertex_info and add it to the open set. + if (!vertex_info.contains(neighbor) || + tentative_g_score < g_score[neighbor]) { + // This path to neighbor is better than any previous one, so we need + // to update our data. Update neighbor's g_score, f_score and previous + // vertex on the path + g_score[neighbor] = tentative_g_score; + auto f_score = tentative_g_score + heuristic(neighbor); + + // always update vertex_info[neighbor] + vertex_info[neighbor] = { + neighbor, // vertex id + f_score, // f_score = tentantive_g_score + h(neighbor) + current.id // neighbor vertex came from current vertex + }; + + open_set.push(vertex_info[neighbor]); + } + } + } + + // No path found + return std::nullopt; +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/bellman_ford.h b/include/graaflib/algorithm/shortest_path/bellman_ford.h new file mode 100644 index 00000000..45b791ca --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/bellman_ford.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +namespace graaf::algorithm { + +/** + * Find the shortest paths from a source vertex to all other vertices using + * the Bellman-Ford algorithm. + * + * @tparam V The vertex type of the graph. + * @tparam E The edge type of the graph. + * @tparam T The graph specialization (directed or undirected). + * @tparam WEIGHT_T The type of weight associated with the edges. + * @param graph The graph in which to find the shortest paths. + * @param start_vertex The source vertex for the shortest paths. + * @return A map of target vertex IDs to shortest path structures. Each + * value contains a graph_path object representing the + * shortest path from the source vertex to the respective vertex. + * If a vertex is unreachable from the source, its entry will be + * absent from the map. + */ +template ()))> +std::unordered_map> +bellman_ford_shortest_paths(const graph& graph, + vertex_id_t start_vertex); + +} // namespace graaf::algorithm + +#include "bellman_ford.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/bellman_ford.tpp b/include/graaflib/algorithm/shortest_path/bellman_ford.tpp new file mode 100644 index 00000000..65fb2297 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/bellman_ford.tpp @@ -0,0 +1,58 @@ +#pragma once + +namespace graaf::algorithm { + +template +std::unordered_map> +bellman_ford_shortest_paths(const graph& graph, + vertex_id_t start_vertex) { + std::unordered_map> shortest_paths; + + const auto found_shorter_path{ + [&shortest_paths](edge_id_t edge_id, const E& edge) { + const auto weight = get_weight(edge); + const auto [u, v]{edge_id}; + return shortest_paths[u].total_weight != + std::numeric_limits::max() && + shortest_paths[u].total_weight + weight < + shortest_paths[v].total_weight; + }}; + + // Initialize the shortest path distances from the starting vertex to + // "infinity" for all vertices + for (const auto& [vertex_id, _] : graph.get_vertices()) { + shortest_paths[vertex_id] = {{vertex_id}, + std::numeric_limits::max()}; + } + + // Set the distance from the starting vertex to itself to 0 + shortest_paths[start_vertex] = {{start_vertex}, 0}; + + // Relax edges for |V| - 1 iterations + for (std::size_t i = 1; i < graph.vertex_count(); ++i) { + for (const auto& [edge_id, edge] : graph.get_edges()) { + const auto [u, v]{edge_id}; + WEIGHT_T weight = get_weight(edge); + + if (found_shorter_path(edge_id, edge)) { + // Update the shortest path to vertex v + shortest_paths[v] = { + {shortest_paths[u].vertices}, + shortest_paths[u].total_weight + weight, + }; + shortest_paths[v].vertices.push_back(v); + } + } + } + // Negative cycle detection by doing an additional pass in the graph + for (const auto& [edge_id, edge] : graph.get_edges()) { + const auto [u, v]{edge_id}; + WEIGHT_T weight = get_weight(edge); + if (found_shorter_path(edge_id, edge)) { + throw std::invalid_argument{"Negative cycle detected in the graph."}; + } + } + return shortest_paths; +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/bfs_shortest_path.h b/include/graaflib/algorithm/shortest_path/bfs_shortest_path.h new file mode 100644 index 00000000..403936d4 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/bfs_shortest_path.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include + +namespace graaf::algorithm { + +/** + * @brief calculates the shortest path between one start_vertex and one + * end_vertex using BFS. This does not consider edge weights. + * + * @param graph The graph to extract shortest path from. + * @param start_vertex Vertex id where the shortest path should start. + * @param end_vertex Vertex id where the shortest path should end. + * @return An optional with the shortest path (list of vertices) if found. + */ +template ()))> +std::optional> bfs_shortest_path( + const graph& graph, vertex_id_t start_vertex, + vertex_id_t end_vertex); + +} // namespace graaf::algorithm + +#include "bfs_shortest_path.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/bfs_shortest_path.tpp b/include/graaflib/algorithm/shortest_path/bfs_shortest_path.tpp new file mode 100644 index 00000000..9b3726e7 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/bfs_shortest_path.tpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace graaf::algorithm { + +template +std::optional> bfs_shortest_path( + const graph& graph, vertex_id_t start_vertex, + vertex_id_t end_vertex) { + std::unordered_map> vertex_info{ + {start_vertex, {start_vertex, 0, start_vertex}}}; + + const auto callback{[&vertex_info](const edge_id_t& edge) { + const auto [source, target]{edge}; + + if (!vertex_info.contains(target)) { + vertex_info[target] = {target, vertex_info[source].dist_from_start + 1, + source}; + } + }}; + + // 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); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/common.h b/include/graaflib/algorithm/shortest_path/common.h new file mode 100644 index 00000000..ad19330d --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/common.h @@ -0,0 +1,45 @@ +#pragma once +#include + +#include +#include + +namespace graaf::algorithm { + +// Forward declaration +template +struct graph_path; + +namespace detail { + +template +struct path_vertex { + vertex_id_t id; + WEIGHT_T dist_from_start; + vertex_id_t prev_id; + + [[nodiscard]] bool operator>(const path_vertex& other) { + return dist_from_start > other.dist_from_start; + } +}; + +template +std::optional> reconstruct_path( + vertex_id_t start_vertex, vertex_id_t end_vertex, + std::unordered_map>& vertex_info); + +} // namespace detail + +template +struct graph_path { + std::list vertices; + WEIGHT_T total_weight; + + bool operator==(const graph_path& other) const { + return vertices == other.vertices && total_weight == other.total_weight; + } +}; + +} // namespace graaf::algorithm + +#include "common.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/common.tpp b/include/graaflib/algorithm/shortest_path/common.tpp new file mode 100644 index 00000000..2529752a --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/common.tpp @@ -0,0 +1,30 @@ +#pragma once + +namespace graaf::algorithm { + +namespace detail { + +template +std::optional> reconstruct_path( + vertex_id_t start_vertex, vertex_id_t end_vertex, + std::unordered_map>& vertex_info) { + if (!vertex_info.contains(end_vertex)) { + return std::nullopt; + } + + graph_path path; + auto current = end_vertex; + + while (current != start_vertex) { + path.vertices.push_front(current); + current = vertex_info[current].prev_id; + } + + path.vertices.push_front(start_vertex); + path.total_weight = vertex_info[end_vertex].dist_from_start; + return path; +} + +} // namespace detail + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/dijkstra_shortest_path.h b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_path.h new file mode 100644 index 00000000..2ce73b97 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_path.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include + +namespace graaf::algorithm { + +/** + * @brief calculates the shortest path between one start_vertex and one + * end_vertex using Dijkstra's algorithm. Works on both weighted as well as + * unweighted graphs. For unweighted graphs, a unit weight is used for each + * edge. + * + * @param graph The graph to extract shortest path from. + * @param start_vertex Vertex id where the shortest path should start. + * @param end_vertex Vertex id where the shortest path should end. + * @return An optional with the shortest path (list of vertices) if found. + */ +template ()))> +std::optional> dijkstra_shortest_path( + const graph& graph, vertex_id_t start_vertex, + vertex_id_t end_vertex); + +} // namespace graaf::algorithm + +#include "dijkstra_shortest_path.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/dijkstra_shortest_path.tpp b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_path.tpp new file mode 100644 index 00000000..bd164983 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_path.tpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +namespace graaf::algorithm { + +template +std::optional> dijkstra_shortest_path( + const graph& graph, vertex_id_t start_vertex, + vertex_id_t end_vertex) { + using weighted_path_item = detail::path_vertex; + using dijkstra_queue_t = + std::priority_queue, + std::greater<>>; + dijkstra_queue_t to_explore{}; + std::unordered_map vertex_info; + + vertex_info[start_vertex] = {start_vertex, 0, start_vertex}; + to_explore.push(vertex_info[start_vertex]); + + while (!to_explore.empty()) { + auto current{to_explore.top()}; + to_explore.pop(); + + if (current.id == end_vertex) { + break; + } + + for (const auto& neighbor : graph.get_neighbors(current.id)) { + WEIGHT_T edge_weight = get_weight(graph.get_edge(current.id, neighbor)); + + if (edge_weight < 0) { + std::ostringstream error_msg; + error_msg << "Negative edge weight [" << edge_weight + << "] between vertices [" << current.id << "] -> [" + << neighbor << "]."; + throw std::invalid_argument{error_msg.str()}; + } + + WEIGHT_T distance = current.dist_from_start + edge_weight; + + if (!vertex_info.contains(neighbor) || + distance < vertex_info[neighbor].dist_from_start) { + vertex_info[neighbor] = {neighbor, distance, current.id}; + to_explore.push(vertex_info[neighbor]); + } + } + } + + return reconstruct_path(start_vertex, end_vertex, vertex_info); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/dijkstra_shortest_paths.h b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_paths.h new file mode 100644 index 00000000..7beab6c4 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_paths.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace graaf::algorithm { + +/** + * Find the shortest paths from a source vertex to all other vertices in the + * graph using Dijkstra's algorithm. + * + * @tparam V The vertex type of the graph. + * @tparam E The edge type of the graph. + * @tparam T The graph type (directed or undirected). + * @tparam WEIGHT_T The type of edge weights. + * @param graph The graph we want to search. + * @param source_vertex The source vertex from which to compute shortest paths. + * @return A map containing the shortest paths from the source vertex to all + * other vertices. The map keys are target vertex IDs, and the values are + * instances of graph_path, representing the shortest distance and the path + * (list of vertex IDs) from the source to the target. If a vertex is not + * reachable from the source, its entry will be absent from the map. + */ +template ()))> +[[nodiscard]] std::unordered_map> +dijkstra_shortest_paths(const graph& graph, vertex_id_t source_vertex); + +} // namespace graaf::algorithm + +#include "dijkstra_shortest_paths.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/shortest_path/dijkstra_shortest_paths.tpp b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_paths.tpp new file mode 100644 index 00000000..50251ad4 --- /dev/null +++ b/include/graaflib/algorithm/shortest_path/dijkstra_shortest_paths.tpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +namespace graaf::algorithm { + +template +[[nodiscard]] std::unordered_map> +dijkstra_shortest_paths(const graph& graph, + vertex_id_t source_vertex) { + std::unordered_map> shortest_paths; + + using weighted_path_item = detail::path_vertex; + using dijkstra_queue_t = + std::priority_queue, + std::greater<>>; + dijkstra_queue_t to_explore{}; + + shortest_paths[source_vertex].total_weight = 0; + shortest_paths[source_vertex].vertices.push_back(source_vertex); + to_explore.push(weighted_path_item{source_vertex, 0}); + + while (!to_explore.empty()) { + auto current{to_explore.top()}; + to_explore.pop(); + + if (shortest_paths.contains(current.id) && + current.dist_from_start > shortest_paths[current.id].total_weight) { + continue; + } + + for (const auto neighbor : graph.get_neighbors(current.id)) { + WEIGHT_T edge_weight = get_weight(graph.get_edge(current.id, neighbor)); + + if (edge_weight < 0) { + std::ostringstream error_msg; + error_msg << "Negative edge weight [" << edge_weight + << "] between vertices [" << current.id << "] -> [" + << neighbor << "]."; + throw std::invalid_argument{error_msg.str()}; + } + + WEIGHT_T distance = current.dist_from_start + edge_weight; + + if (!shortest_paths.contains(neighbor) || + distance < shortest_paths[neighbor].total_weight) { + shortest_paths[neighbor].total_weight = distance; + shortest_paths[neighbor].vertices = shortest_paths[current.id].vertices; + shortest_paths[neighbor].vertices.push_back(neighbor); + to_explore.push(weighted_path_item{neighbor, distance}); + } + } + } + + return shortest_paths; +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/include/graaflib/algorithm/strongly_connected_components.h b/include/graaflib/algorithm/strongly_connected_components/tarjan.h similarity index 95% rename from include/graaflib/algorithm/strongly_connected_components.h rename to include/graaflib/algorithm/strongly_connected_components/tarjan.h index 7a42331e..15425276 100644 --- a/include/graaflib/algorithm/strongly_connected_components.h +++ b/include/graaflib/algorithm/strongly_connected_components/tarjan.h @@ -27,4 +27,4 @@ tarjans_strongly_connected_components( } // namespace graaf::algorithm -#include "strongly_connected_components.tpp" \ No newline at end of file +#include "tarjan.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/strongly_connected_components.tpp b/include/graaflib/algorithm/strongly_connected_components/tarjan.tpp similarity index 97% rename from include/graaflib/algorithm/strongly_connected_components.tpp rename to include/graaflib/algorithm/strongly_connected_components/tarjan.tpp index 536ebfbe..50358f51 100644 --- a/include/graaflib/algorithm/strongly_connected_components.tpp +++ b/include/graaflib/algorithm/strongly_connected_components/tarjan.tpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include // For std::function #include diff --git a/include/graaflib/algorithm/topological_sorting.h b/include/graaflib/algorithm/topological_sorting/dfs_topological_sorting.h similarity index 80% rename from include/graaflib/algorithm/topological_sorting.h rename to include/graaflib/algorithm/topological_sorting/dfs_topological_sorting.h index 5cae4ae9..7973d6cc 100644 --- a/include/graaflib/algorithm/topological_sorting.h +++ b/include/graaflib/algorithm/topological_sorting/dfs_topological_sorting.h @@ -15,8 +15,8 @@ namespace graaf::algorithm { * @return Vector of vertices sorted in topological order */ template -[[nodiscard]] std::optional> topological_sort( +[[nodiscard]] std::optional> dfs_topological_sort( const graph& graph); } // namespace graaf::algorithm -#include "topological_sorting.tpp" +#include "dfs_topological_sorting.tpp" diff --git a/include/graaflib/algorithm/topological_sorting.tpp b/include/graaflib/algorithm/topological_sorting/dfs_topological_sorting.tpp similarity index 87% rename from include/graaflib/algorithm/topological_sorting.tpp rename to include/graaflib/algorithm/topological_sorting/dfs_topological_sorting.tpp index 6dd26c72..c1180cba 100644 --- a/include/graaflib/algorithm/topological_sorting.tpp +++ b/include/graaflib/algorithm/topological_sorting/dfs_topological_sorting.tpp @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include @@ -31,7 +31,7 @@ void do_dfs_topological_sort( }; // namespace detail template -std::optional> topological_sort( +std::optional> dfs_topological_sort( const graph& graph) { // Graph should be acyclic if (dfs_cycle_detection(graph)) { diff --git a/test/graaflib/algorithm/graph_coloring_test.cpp b/test/graaflib/algorithm/coloring/greedy_coloring_test.cpp similarity index 100% rename from test/graaflib/algorithm/graph_coloring_test.cpp rename to test/graaflib/algorithm/coloring/greedy_coloring_test.cpp diff --git a/test/graaflib/algorithm/cycle_detection_test.cpp b/test/graaflib/algorithm/cycle_detection/dfs_cycle_detection_test.cpp similarity index 98% rename from test/graaflib/algorithm/cycle_detection_test.cpp rename to test/graaflib/algorithm/cycle_detection/dfs_cycle_detection_test.cpp index 4a75da8e..d39170c7 100644 --- a/test/graaflib/algorithm/cycle_detection_test.cpp +++ b/test/graaflib/algorithm/cycle_detection/dfs_cycle_detection_test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/test/graaflib/algorithm/graph_traversal_test.cpp b/test/graaflib/algorithm/graph_traversal/breadth_first_search_test.cpp similarity index 55% rename from test/graaflib/algorithm/graph_traversal_test.cpp rename to test/graaflib/algorithm/graph_traversal/breadth_first_search_test.cpp index d1b04a7b..b4281dbc 100644 --- a/test/graaflib/algorithm/graph_traversal_test.cpp +++ b/test/graaflib/algorithm/graph_traversal/breadth_first_search_test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -14,13 +14,13 @@ namespace graaf::algorithm { namespace { template -struct TypedGraphTraversalTest : public testing::Test { +struct TypedGraphTraversalTestBFS : public testing::Test { using graph_t = T; }; using graph_types = testing::Types, undirected_graph>; -TYPED_TEST_SUITE(TypedGraphTraversalTest, graph_types); +TYPED_TEST_SUITE(TypedGraphTraversalTestBFS, graph_types); using seen_edges_t = std::unordered_multiset; using edge_order_t = std::unordered_map; @@ -44,26 +44,7 @@ struct record_edge_callback { } // namespace -TYPED_TEST(TypedGraphTraversalTest, MinimalGraphDFS) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - graph_t graph{}; - - const auto vertex_1{graph.add_vertex(10)}; - - seen_edges_t seen_edges{}; - edge_order_t edge_order{}; - - // WHEN - depth_first_traverse(graph, vertex_1, - record_edge_callback{seen_edges, edge_order}); - - // THEN - const seen_edges_t expected_edges{}; - ASSERT_EQ(seen_edges, expected_edges); -} - -TYPED_TEST(TypedGraphTraversalTest, MinimalGraphBFS) { +TYPED_TEST(TypedGraphTraversalTestBFS, MinimalGraphBFS) { // GIVEN using graph_t = typename TestFixture::graph_t; graph_t graph{}; @@ -82,31 +63,7 @@ TYPED_TEST(TypedGraphTraversalTest, MinimalGraphBFS) { ASSERT_EQ(seen_edges, expected_edges); } -TYPED_TEST(TypedGraphTraversalTest, SimpleGraphDFS) { - // 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)}; - - // We add an edge from the vertex where we start the traversal - // so it does not matter whether this is a directed or undirected graph - graph.add_edge(vertex_1, vertex_2, 100); - - seen_edges_t seen_edges{}; - edge_order_t edge_order{}; - - // WHEN - depth_first_traverse(graph, vertex_1, - record_edge_callback{seen_edges, edge_order}); - - // THEN - const seen_edges_t expected_edges{{vertex_1, vertex_2}}; - ASSERT_EQ(seen_edges, expected_edges); -} - -TYPED_TEST(TypedGraphTraversalTest, SimpleGraphBFS) { +TYPED_TEST(TypedGraphTraversalTestBFS, SimpleGraphBFS) { // GIVEN using graph_t = typename TestFixture::graph_t; graph_t graph{}; @@ -129,27 +86,6 @@ TYPED_TEST(TypedGraphTraversalTest, SimpleGraphBFS) { ASSERT_EQ(seen_edges, expected_edges); } -TEST(GraphTraversalTest, DirectedGraphEdgeWrongDirectionDFS) { - directed_graph graph{}; - - const auto vertex_1{graph.add_vertex(10)}; - const auto vertex_2{graph.add_vertex(20)}; - - // The direction of the edge is from 2 -> 1 - graph.add_edge(vertex_2, vertex_1, 100); - - seen_edges_t seen_edges{}; - edge_order_t edge_order{}; - - // WHEN - here vertex 1 has no neighbors due to the edge direction - depth_first_traverse(graph, vertex_1, - record_edge_callback{seen_edges, edge_order}); - - // THEN - there was no edge to traverse - const seen_edges_t expected_edges{}; - ASSERT_EQ(seen_edges, expected_edges); -} - TEST(GraphTraversalTest, DirectedGraphEdgeWrongDirectionBFS) { directed_graph graph{}; @@ -171,35 +107,7 @@ TEST(GraphTraversalTest, DirectedGraphEdgeWrongDirectionBFS) { ASSERT_EQ(seen_edges, expected_edges); } -TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFS) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - const auto [graph, - vertex_ids]{utils::scenarios::create_tree_scenario()}; - - seen_edges_t seen_edges{}; - edge_order_t edge_order{}; - - // WHEN - depth_first_traverse(graph, vertex_ids[0], - record_edge_callback{seen_edges, edge_order}); - - // THEN - 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_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) { +TYPED_TEST(TypedGraphTraversalTestBFS, MoreComplexGraphBFS) { // GIVEN using graph_t = typename TestFixture::graph_t; const auto [graph, @@ -231,30 +139,8 @@ TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphBFS) { 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]{utils::scenarios::create_tree_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) { +TYPED_TEST(TypedGraphTraversalTestBFS, + MoreComplexGraphBFSImmediateTermination) { // GIVEN using graph_t = typename TestFixture::graph_t; const auto [graph, @@ -277,7 +163,7 @@ TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphBFSImmediateTermination) { ASSERT_EQ(seen_edges, expected_edges); } -TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSTermination) { +TYPED_TEST(TypedGraphTraversalTestBFS, MoreComplexGraphBFSTermination) { // GIVEN using graph_t = typename TestFixture::graph_t; const auto [graph, @@ -305,7 +191,7 @@ TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSTermination) { seen_edges == expected_edges_option_2); } -TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSLaterTermination) { +TYPED_TEST(TypedGraphTraversalTestBFS, MoreComplexGraphBFSLaterTermination) { // GIVEN using graph_t = typename TestFixture::graph_t; const auto [graph, @@ -334,64 +220,6 @@ TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphDFSLaterTermination) { } } -TYPED_TEST(TypedGraphTraversalTest, MoreComplexGraphBFSTermination) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - const auto [graph, - vertex_ids]{utils::scenarios::create_tree_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) { - // GIVEN - directed_graph 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_5, vertex_3, 400); // The direction here is from 5 -> 3 - - seen_edges_t seen_edges{}; - edge_order_t edge_order{}; - - // WHEN - depth_first_traverse(graph, vertex_1, - 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}}; - ASSERT_EQ(seen_edges, expected_edges); -} - TEST(GraphTraversalTest, MoreComplexDirectedGraphEdgeWrongDirectionBFS) { // GIVEN directed_graph graph{}; diff --git a/test/graaflib/algorithm/graph_traversal/depth_first_search_test.cpp b/test/graaflib/algorithm/graph_traversal/depth_first_search_test.cpp new file mode 100644 index 00000000..2d9ec287 --- /dev/null +++ b/test/graaflib/algorithm/graph_traversal/depth_first_search_test.cpp @@ -0,0 +1,221 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace graaf::algorithm { + +namespace { + +template +struct TypedGraphTraversalTestDFS : public testing::Test { + using graph_t = T; +}; + +using graph_types = + testing::Types, undirected_graph>; +TYPED_TEST_SUITE(TypedGraphTraversalTestDFS, graph_types); + +using seen_edges_t = std::unordered_multiset; +using edge_order_t = std::unordered_map; + +/** + * @brief Callback to record the traversed edges and their order. + */ +struct record_edge_callback { + seen_edges_t& seen_edges; + edge_order_t& edge_order; + mutable std::size_t edge_order_supplier{0}; + + record_edge_callback(seen_edges_t& seen_edges, edge_order_t& edge_order) + : seen_edges{seen_edges}, edge_order{edge_order} {} + + void operator()(const edge_id_t& edge) const { + seen_edges.insert(edge); + edge_order[edge] = edge_order_supplier++; + } +}; + +} // namespace + +TYPED_TEST(TypedGraphTraversalTestDFS, MinimalGraphDFS) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + graph_t graph{}; + + const auto vertex_1{graph.add_vertex(10)}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // WHEN + depth_first_traverse(graph, vertex_1, + record_edge_callback{seen_edges, edge_order}); + + // THEN + const seen_edges_t expected_edges{}; + ASSERT_EQ(seen_edges, expected_edges); +} + +TYPED_TEST(TypedGraphTraversalTestDFS, SimpleGraphDFS) { + // 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)}; + + // We add an edge from the vertex where we start the traversal + // so it does not matter whether this is a directed or undirected graph + graph.add_edge(vertex_1, vertex_2, 100); + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // WHEN + depth_first_traverse(graph, vertex_1, + record_edge_callback{seen_edges, edge_order}); + + // THEN + const seen_edges_t expected_edges{{vertex_1, vertex_2}}; + ASSERT_EQ(seen_edges, expected_edges); +} + +TEST(GraphTraversalTest, DirectedGraphEdgeWrongDirectionDFS) { + directed_graph graph{}; + + const auto vertex_1{graph.add_vertex(10)}; + const auto vertex_2{graph.add_vertex(20)}; + + // The direction of the edge is from 2 -> 1 + graph.add_edge(vertex_2, vertex_1, 100); + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // WHEN - here vertex 1 has no neighbors due to the edge direction + depth_first_traverse(graph, vertex_1, + record_edge_callback{seen_edges, edge_order}); + + // THEN - there was no edge to traverse + const seen_edges_t expected_edges{}; + ASSERT_EQ(seen_edges, expected_edges); +} + +TYPED_TEST(TypedGraphTraversalTestDFS, MoreComplexGraphDFS) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, + vertex_ids]{utils::scenarios::create_tree_scenario()}; + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // WHEN + depth_first_traverse(graph, vertex_ids[0], + record_edge_callback{seen_edges, edge_order}); + + // THEN + 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_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(TypedGraphTraversalTestDFS, MoreComplexGraphDFSTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, + vertex_ids]{utils::scenarios::create_tree_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 + depth_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(TypedGraphTraversalTestDFS, + MoreComplexGraphDFSImmediateTermination) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + const auto [graph, + vertex_ids]{utils::scenarios::create_tree_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); +} + +TEST(GraphTraversalTest, MoreComplexDirectedGraphEdgeWrongDirectionDFS) { + // GIVEN + directed_graph 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_5, vertex_3, 400); // The direction here is from 5 -> 3 + + seen_edges_t seen_edges{}; + edge_order_t edge_order{}; + + // WHEN + depth_first_traverse(graph, vertex_1, + 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}}; + ASSERT_EQ(seen_edges, expected_edges); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/minimum_spanning_tree_test.cpp b/test/graaflib/algorithm/minimum_spanning_tree/kruskal_test.cpp similarity index 59% rename from test/graaflib/algorithm/minimum_spanning_tree_test.cpp rename to test/graaflib/algorithm/minimum_spanning_tree/kruskal_test.cpp index bee84e03..1156f2f3 100644 --- a/test/graaflib/algorithm/minimum_spanning_tree_test.cpp +++ b/test/graaflib/algorithm/minimum_spanning_tree/kruskal_test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -11,32 +11,6 @@ namespace graaf::algorithm { -namespace { - -using edge_set_t = std::unordered_set; - -[[nodiscard]] bool compare_mst(const std::vector& actual_mst, - edge_set_t expected_edges) { - for (const auto& edge : actual_mst) { - if (expected_edges.contains(edge)) { - expected_edges.erase(edge); - continue; - } else if (const edge_id_t inverse_edge{edge.second, edge.first}; - expected_edges.contains(inverse_edge)) { - // Since the graph is undirected, we also check the inverse of the edge - expected_edges.erase(inverse_edge); - continue; - } - - // The expected edges did not contain the edge, nor the inverse edge - return false; - } - - return expected_edges.empty(); -} - -} // namespace - template class my_weighted_edge : public weighted_edge { public: @@ -229,125 +203,4 @@ TYPED_TEST(MSTTest, CustomEdgeVertexOneComponentGraph) { ASSERT_EQ(expected_mst, mst); } -TEST(PrimMstTest, SingleVertex) { - // GIVEN - using graph_t = undirected_graph; - - graph_t graph{}; - const auto start_vertex{graph.add_vertex(10)}; - - // WHEN - const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; - - // THEN - The mst is an empty edge collection - ASSERT_TRUE(mst.has_value()); - ASSERT_TRUE(mst->empty()); -} - -TEST(PrimMstTest, DisconnectedGraph) { - // GIVEN - using graph_t = undirected_graph; - - graph_t graph{}; - const auto start_vertex{graph.add_vertex(10)}; - [[maybe_unused]] const auto vertex_1{graph.add_vertex(20)}; - [[maybe_unused]] const auto vertex_2{graph.add_vertex(30)}; - - graph.add_edge(start_vertex, vertex_1, 100); - - // WHEN - const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; - - // THEN - No MST as we cannot span the graph - ASSERT_FALSE(mst.has_value()); -} - -TEST(PrimMstTest, SingleEdge) { - // GIVEN - using graph_t = undirected_graph; - - graph_t graph{}; - - const auto start_vertex{graph.add_vertex(10)}; - const auto vertex_1{graph.add_vertex(20)}; - - graph.add_edge(start_vertex, vertex_1, 100); - - // WHEN - const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; - - // THEN - ASSERT_TRUE(mst.has_value()); - - const edge_set_t expected_edges{{start_vertex, vertex_1}}; - ASSERT_TRUE(compare_mst(mst.value(), expected_edges)); -} - -TEST(PrimMstTest, TreeGraphStartAtRoot) { - // GIVEN - using graph_t = undirected_graph; - const auto [graph, - vertex_ids]{utils::scenarios::create_tree_scenario()}; - - // We start at the root node of the tree - const auto start_vertex{vertex_ids[0]}; - - // WHEN - const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; - - // THEN - Since the graph is a tree we expect all edges in the graph - ASSERT_TRUE(mst.has_value()); - - const edge_set_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_TRUE(compare_mst(mst.value(), expected_edges)); -} - -TEST(PrimMstTest, TreeGraphStartAtLeaf) { - // GIVEN - using graph_t = undirected_graph; - const auto [graph, - vertex_ids]{utils::scenarios::create_tree_scenario()}; - - // We start at a leaf node of the tree - const auto start_vertex{vertex_ids[3]}; - - // WHEN - const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; - - // THEN - Since the graph is a tree we expect all edges in the graph - ASSERT_TRUE(mst.has_value()); - - const edge_set_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_TRUE(compare_mst(mst.value(), expected_edges)); -} - -TEST(PrimMstTest, SimpleGraph) { - // GIVEN - using graph_t = undirected_graph; - const auto [graph, vertex_ids]{ - utils::scenarios::create_simple_graph_scenario()}; - const auto start_vertex{vertex_ids[1]}; - - // WHEN - const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; - - // THEN - ASSERT_TRUE(mst.has_value()); - - const edge_set_t expected_edges{{vertex_ids[1], vertex_ids[0]}, - {vertex_ids[1], vertex_ids[2]}, - {vertex_ids[2], vertex_ids[3]}, - {vertex_ids[3], vertex_ids[4]}}; - - ASSERT_TRUE(compare_mst(mst.value(), expected_edges)); -} - } // namespace graaf::algorithm diff --git a/test/graaflib/algorithm/minimum_spanning_tree/prim_test.cpp b/test/graaflib/algorithm/minimum_spanning_tree/prim_test.cpp new file mode 100644 index 00000000..cb9e1794 --- /dev/null +++ b/test/graaflib/algorithm/minimum_spanning_tree/prim_test.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace graaf::algorithm { + +namespace { + +using edge_set_t = std::unordered_set; + +[[nodiscard]] bool compare_mst(const std::vector& actual_mst, + edge_set_t expected_edges) { + for (const auto& edge : actual_mst) { + if (expected_edges.contains(edge)) { + expected_edges.erase(edge); + continue; + } else if (const edge_id_t inverse_edge{edge.second, edge.first}; + expected_edges.contains(inverse_edge)) { + // Since the graph is undirected, we also check the inverse of the edge + expected_edges.erase(inverse_edge); + continue; + } + + // The expected edges did not contain the edge, nor the inverse edge + return false; + } + + return expected_edges.empty(); +} + +} // namespace + +TEST(PrimMstTest, SingleVertex) { + // GIVEN + using graph_t = undirected_graph; + + graph_t graph{}; + const auto start_vertex{graph.add_vertex(10)}; + + // WHEN + const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; + + // THEN - The mst is an empty edge collection + ASSERT_TRUE(mst.has_value()); + ASSERT_TRUE(mst->empty()); +} + +TEST(PrimMstTest, DisconnectedGraph) { + // GIVEN + using graph_t = undirected_graph; + + graph_t graph{}; + const auto start_vertex{graph.add_vertex(10)}; + [[maybe_unused]] const auto vertex_1{graph.add_vertex(20)}; + [[maybe_unused]] const auto vertex_2{graph.add_vertex(30)}; + + graph.add_edge(start_vertex, vertex_1, 100); + + // WHEN + const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; + + // THEN - No MST as we cannot span the graph + ASSERT_FALSE(mst.has_value()); +} + +TEST(PrimMstTest, SingleEdge) { + // GIVEN + using graph_t = undirected_graph; + + graph_t graph{}; + + const auto start_vertex{graph.add_vertex(10)}; + const auto vertex_1{graph.add_vertex(20)}; + + graph.add_edge(start_vertex, vertex_1, 100); + + // WHEN + const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; + + // THEN + ASSERT_TRUE(mst.has_value()); + + const edge_set_t expected_edges{{start_vertex, vertex_1}}; + ASSERT_TRUE(compare_mst(mst.value(), expected_edges)); +} + +TEST(PrimMstTest, TreeGraphStartAtRoot) { + // GIVEN + using graph_t = undirected_graph; + const auto [graph, + vertex_ids]{utils::scenarios::create_tree_scenario()}; + + // We start at the root node of the tree + const auto start_vertex{vertex_ids[0]}; + + // WHEN + const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; + + // THEN - Since the graph is a tree we expect all edges in the graph + ASSERT_TRUE(mst.has_value()); + + const edge_set_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_TRUE(compare_mst(mst.value(), expected_edges)); +} + +TEST(PrimMstTest, TreeGraphStartAtLeaf) { + // GIVEN + using graph_t = undirected_graph; + const auto [graph, + vertex_ids]{utils::scenarios::create_tree_scenario()}; + + // We start at a leaf node of the tree + const auto start_vertex{vertex_ids[3]}; + + // WHEN + const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; + + // THEN - Since the graph is a tree we expect all edges in the graph + ASSERT_TRUE(mst.has_value()); + + const edge_set_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_TRUE(compare_mst(mst.value(), expected_edges)); +} + +TEST(PrimMstTest, SimpleGraph) { + // GIVEN + using graph_t = undirected_graph; + const auto [graph, vertex_ids]{ + utils::scenarios::create_simple_graph_scenario()}; + const auto start_vertex{vertex_ids[1]}; + + // WHEN + const auto mst{prim_minimum_spanning_tree(graph, start_vertex)}; + + // THEN + ASSERT_TRUE(mst.has_value()); + + const edge_set_t expected_edges{{vertex_ids[1], vertex_ids[0]}, + {vertex_ids[1], vertex_ids[2]}, + {vertex_ids[2], vertex_ids[3]}, + {vertex_ids[3], vertex_ids[4]}}; + + ASSERT_TRUE(compare_mst(mst.value(), expected_edges)); +} + +} // namespace graaf::algorithm diff --git a/test/graaflib/algorithm/shortest_path/a_star_test.cpp b/test/graaflib/algorithm/shortest_path/a_star_test.cpp new file mode 100644 index 00000000..0c87e7d7 --- /dev/null +++ b/test/graaflib/algorithm/shortest_path/a_star_test.cpp @@ -0,0 +1,336 @@ +#include +#include +#include + +namespace graaf::algorithm { + +namespace { + +template +class my_weighted_edge : public weighted_edge { + public: + explicit my_weighted_edge(T weight) : weight_{weight} {} + + [[nodiscard]] T get_weight() const noexcept override { return weight_; } + + private: + T weight_{}; +}; + +template +struct AStarShortestPathTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(AStarShortestPathTest, weighted_graph_types); + +template +struct AStarShortestPathSignedTypesTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_signed_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(AStarShortestPathSignedTypesTest, weighted_graph_signed_types); + +} // namespace + +// Graph with only one vertex. +TYPED_TEST(AStarShortestPathTest, AStarMinimalShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + + // WHEN + // The weight_t type annotation and static_cast are needed to avoid compiler + // warnings. + const auto heuristic = [](vertex_id_t vertex) -> weight_t { + return static_cast(0); + }; + const auto path = a_star_search(graph, vertex_id_1, vertex_id_1, heuristic); + + // THEN + const graph_path expected_path{{vertex_id_1}, 0}; + ASSERT_EQ(path, expected_path); +} + +// Find the shortest path between the only two vertices in a graph. +TYPED_TEST(AStarShortestPathTest, AStarSimpleShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + + const auto heuristic = [](vertex_id_t vertex) -> weight_t { + return static_cast(0); + }; + + // WHEN + const auto path = a_star_search(graph, vertex_id_1, vertex_id_2, heuristic); + + // THEN + const graph_path expected_path{{vertex_id_1, vertex_id_2}, 1}; + ASSERT_EQ(path, expected_path); +} + +// Graph where there's no path between the start and target vertices. +TYPED_TEST(AStarShortestPathTest, NoPathExistence) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + // Define start and target vertices + vertex_id_t start_vertex = 0; + vertex_id_t target_vertex = 5; + + // Define a heuristic function that always returns 0 + auto zero_heuristic = [](vertex_id_t vertex) -> weight_t { + return static_cast(0); + }; + + // WHEN + auto result = + a_star_search(graph, start_vertex, target_vertex, zero_heuristic); + + // THEN + // Check that the result is an empty optional + ASSERT_FALSE(result.has_value()); +} + +// Find the shortest path between multiple possible paths in a graph. +TYPED_TEST(AStarShortestPathTest, MultiplePathsTest) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(2)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); + + // WHEN + const auto heuristic = [](vertex_id_t vertex) -> weight_t { + return static_cast(0); + }; + const auto path = a_star_search(graph, vertex_id_1, vertex_id_3, heuristic); + + // THEN + const graph_path expected_path{{vertex_id_1, vertex_id_3}, 2}; + ASSERT_EQ(path, expected_path); +} + +// Suboptimal Path Test +TYPED_TEST(AStarShortestPathTest, AStarSuboptimalPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(2)}); + graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(4)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(3)}); + + // WHEN + const auto heuristic = [](vertex_id_t vertex) -> weight_t { + return static_cast(10); // Overestimate the remaining cost + }; + const auto path = a_star_search(graph, vertex_id_1, vertex_id_3, heuristic); + + // THEN + ASSERT_TRUE(path.has_value()); // Check if optional has a value + // Note: The path might not be the shortest, but it should still be valid +} + +// Negative Weight Test +TYPED_TEST(AStarShortestPathSignedTypesTest, AStarNegativeWeight) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, + edge_t{static_cast(-1)}); // Negative weight edge + + const auto heuristic = [](vertex_id_t vertex) -> weight_t { + return static_cast(0); + }; + + // THEN + // Taken from DijkstraNegativeWeight + ASSERT_THROW( + { + try { + // Call the get_edge function for non-existing vertices + [[maybe_unused]] const auto path{ + a_star_search(graph, vertex_id_1, vertex_id_2, heuristic)}; + // If the above line doesn't throw an exception, fail the test + FAIL() + << "Expected std::invalid_argument exception, but no exception " + "was thrown."; + } catch (const std::invalid_argument &ex) { + // Verify that the exception message contains the expected error + // message + EXPECT_EQ( + ex.what(), + fmt::format( + "Negative edge weight [{}] between vertices [{}] -> [{}].", + -1, vertex_id_1, vertex_id_2)); + throw; + } + }, + std::invalid_argument); +} + +// Heuristic Impact Test +TYPED_TEST(AStarShortestPathTest, AStarHeuristicImpact) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + const auto vertex_id_4{graph.add_vertex(40)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); + graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); + graph.add_edge(vertex_id_1, vertex_id_4, edge_t{static_cast(2)}); + graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(1)}); + + const auto start_vertex = vertex_id_1; + const auto target_vertex = vertex_id_3; + + // Define two different heuristic functions + const auto heuristic1 = [](vertex_id_t vertex) -> weight_t { + return static_cast(0); + }; // Underestimating heuristic + const auto heuristic2 = [](vertex_id_t vertex) -> weight_t { + return static_cast(10); + }; // Overestimating heuristic + + // WHEN + const auto path_with_underestimating_heuristic = + a_star_search(graph, start_vertex, target_vertex, heuristic1); + const auto path_with_overestimating_heuristic = + a_star_search(graph, start_vertex, target_vertex, heuristic2); + + // THEN + // Verify that the path with the underestimating heuristic is shorter + ASSERT_TRUE(path_with_underestimating_heuristic.has_value()); + ASSERT_TRUE(path_with_overestimating_heuristic.has_value()); + ASSERT_LT(path_with_underestimating_heuristic->total_weight, + path_with_overestimating_heuristic->total_weight); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/shortest_path/bellman_ford_test.cpp b/test/graaflib/algorithm/shortest_path/bellman_ford_test.cpp new file mode 100644 index 00000000..777c2f96 --- /dev/null +++ b/test/graaflib/algorithm/shortest_path/bellman_ford_test.cpp @@ -0,0 +1,227 @@ +#include +#include + +namespace graaf::algorithm { + +namespace { + +template +class my_weighted_edge : public weighted_edge { + public: + explicit my_weighted_edge(T weight) : weight_{weight} {} + + [[nodiscard]] T get_weight() const noexcept override { return weight_; } + + private: + T weight_{}; +}; + +template +struct BellmanFordShortestPathsTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(BellmanFordShortestPathsTest, weighted_graph_types); + +template +struct BellmanFordShortestPathsSignedTypesTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; +using directed_weighted_graph_signed_types = testing::Types< + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(BellmanFordShortestPathsSignedTypesTest, + directed_weighted_graph_signed_types); + +} // namespace + +TYPED_TEST(BellmanFordShortestPathsTest, BellmanFordMinimalShortestPathsTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + + // WHEN; + const auto path_map = bellman_ford_shortest_paths(graph, vertex_id_1); + + // THEN + const graph_path path1{{vertex_id_1}, 0}; + std::unordered_map> expected_path_map; + expected_path_map[vertex_id_1] = path1; + ASSERT_EQ(path_map, expected_path_map); +} + +TYPED_TEST(BellmanFordShortestPathsTest, BellmanFordShortestPathsTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(3)}); + + // WHEN + const auto path_map = bellman_ford_shortest_paths(graph, vertex_id_1); + + // THEN + const graph_path path1{{vertex_id_1}, 0}; + const graph_path path2{{vertex_id_1, vertex_id_2}, 3}; + + std::unordered_map> expected_path_map; + expected_path_map[vertex_id_1] = path1; + expected_path_map[vertex_id_2] = path2; + ASSERT_EQ(path_map, expected_path_map); +} + +TYPED_TEST(BellmanFordShortestPathsTest, + BellmanFordMoreComplexShortestPathTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + const auto vertex_id_4{graph.add_vertex(40)}; + const auto vertex_id_5{graph.add_vertex(50)}; + + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); + graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(4)}); + graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast(5)}); + graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(6)}); + + // WHEN + const auto path_map = bellman_ford_shortest_paths(graph, vertex_id_1); + + // THEN + const graph_path path1{{vertex_id_1}, 0}; + const graph_path path2{{vertex_id_1, vertex_id_2}, 1}; + const graph_path path3{{vertex_id_1, vertex_id_2, vertex_id_3}, 2}; + const graph_path path4{ + {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_4}, 6}; + const graph_path path5{ + {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8}; + + std::unordered_map> expected_path_map; + expected_path_map[vertex_id_1] = path1; + expected_path_map[vertex_id_2] = path2; + expected_path_map[vertex_id_3] = path3; + expected_path_map[vertex_id_4] = path4; + expected_path_map[vertex_id_5] = path5; + ASSERT_EQ(path_map, expected_path_map); +} + +TYPED_TEST(BellmanFordShortestPathsSignedTypesTest, + BellmanFordNegativecycleTest) { + // Bellman Ford can handle negative edge weights only in directed graphs. + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + // Adding vertices + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + const auto vertex_id_4{graph.add_vertex(40)}; + + // Adding Edges + + // Negative cycle exists between the vertices 2,3 and 4. + graph.add_edge(vertex_id_2, vertex_id_1, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(-6)}); + graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(-1)}); + graph.add_edge(vertex_id_4, vertex_id_2, edge_t{static_cast(-2)}); + + ASSERT_THROW( + { + try { + [[maybe_unused]] const auto path{ + bellman_ford_shortest_paths(graph, vertex_id_3)}; + // If the above line doesn't throw an exception, fail the test + FAIL() << "Expected std::invalid_argument exception, but no " + "exception was thrown"; + } catch (const std::invalid_argument &ex) { + // Verify that the exception message contains the expected err or + // message. + EXPECT_STREQ("Negative cycle detected in the graph.", ex.what()); + throw; + } + }, + std::invalid_argument); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/shortest_path/bfs_shortest_path_test.cpp b/test/graaflib/algorithm/shortest_path/bfs_shortest_path_test.cpp new file mode 100644 index 00000000..eb0310e0 --- /dev/null +++ b/test/graaflib/algorithm/shortest_path/bfs_shortest_path_test.cpp @@ -0,0 +1,151 @@ +#include +#include +#include +#include + +namespace graaf::algorithm { + +namespace { +template +struct TypedShortestPathTest : public testing::Test { + using graph_t = T; +}; + +using graph_types = + testing::Types, undirected_graph>; +TYPED_TEST_SUITE(TypedShortestPathTest, graph_types); +} // namespace + +TYPED_TEST(TypedShortestPathTest, BfsMinimalShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + graph_t graph{}; + + const auto vertex_1{graph.add_vertex(10)}; + + // WHEN + const auto path = bfs_shortest_path(graph, vertex_1, vertex_1); + + // THEN + const graph_path expected_path{{vertex_1}, 0}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(TypedShortestPathTest, BfsNoAvailablePath) { + // 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)}; + + // WHEN + const auto path = bfs_shortest_path(graph, vertex_1, vertex_2); + + // THEN + ASSERT_FALSE(path.has_value()); +} + +TYPED_TEST(TypedShortestPathTest, BfsSimpleShortestPath) { + // 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)}; + + // We add an edge from the vertex where we start the traversal + // so it does not matter whether this is a directed or undirected graph + graph.add_edge(vertex_1, vertex_2, 100); + + // WHEN + const auto path = bfs_shortest_path(graph, vertex_1, vertex_2); + + // THEN + const graph_path expected_path{{vertex_1, vertex_2}, 1}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(TypedShortestPathTest, BfsMoreComplexShortestPath) { + // 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_2, vertex_3, 200); + graph.add_edge(vertex_1, vertex_3, 300); + graph.add_edge(vertex_3, vertex_4, 400); + graph.add_edge(vertex_4, vertex_5, 500); + graph.add_edge(vertex_3, vertex_5, 600); + + // WHEN + const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); + + // THEN + const graph_path expected_path{{vertex_1, vertex_3, vertex_5}, 2}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(TypedShortestPathTest, BfsCyclicShortestPath) { + // 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_2, vertex_3, 200); + graph.add_edge(vertex_3, vertex_4, 300); + graph.add_edge(vertex_4, vertex_2, 300); + graph.add_edge(vertex_3, vertex_5, 400); + + // WHEN + const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); + + // THEN + const graph_path expected_path{{vertex_1, vertex_2, vertex_3, vertex_5}, + 3}; + ASSERT_EQ(path, expected_path); +} + +TEST(ShortestPathTest, BfsDirectedrWrongDirectionShortestPath) { + // GIVEN + directed_graph 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)}; + + // Edge between 2 and 3 is inverted, so path needs to take detour via 4 + graph.add_edge(vertex_1, vertex_2, 100); + graph.add_edge(vertex_3, vertex_2, 200); + graph.add_edge(vertex_3, vertex_5, 300); + graph.add_edge(vertex_2, vertex_4, 400); + graph.add_edge(vertex_4, vertex_3, 500); + + // WHEN + const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); + + // THEN + const graph_path expected_path{ + {vertex_1, vertex_2, vertex_4, vertex_3, vertex_5}, 4}; + ASSERT_EQ(path, expected_path); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/shortest_path/dijkstra_shortest_path_test.cpp b/test/graaflib/algorithm/shortest_path/dijkstra_shortest_path_test.cpp new file mode 100644 index 00000000..bdba4e15 --- /dev/null +++ b/test/graaflib/algorithm/shortest_path/dijkstra_shortest_path_test.cpp @@ -0,0 +1,269 @@ +#include +#include +#include + +namespace graaf::algorithm { + +namespace { + +template +class my_weighted_edge : public weighted_edge { + public: + explicit my_weighted_edge(T weight) : weight_{weight} {} + + [[nodiscard]] T get_weight() const noexcept override { return weight_; } + + private: + T weight_{}; +}; + +template +struct DijkstraShortestPathTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(DijkstraShortestPathTest, weighted_graph_types); + +template +struct DijkstraShortestPathSignedTypesTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_signed_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(DijkstraShortestPathSignedTypesTest, + weighted_graph_signed_types); + +} // namespace + +TYPED_TEST(DijkstraShortestPathTest, DijkstraMinimalShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + + // WHEN; + const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_1); + + // THEN + const graph_path expected_path{{vertex_id_1}, 0}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(DijkstraShortestPathTest, DijkstraNoAvailablePath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + + // WHEN; + const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_2); + + // THEN + ASSERT_FALSE(path.has_value()); +} + +TYPED_TEST(DijkstraShortestPathTest, DijkstraSimpleShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(3)}); + + // WHEN + const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_2); + + // THEN + const graph_path expected_path{{vertex_id_1, vertex_id_2}, 3}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(DijkstraShortestPathTest, DijkstraMoreComplexShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + const auto vertex_id_4{graph.add_vertex(40)}; + const auto vertex_id_5{graph.add_vertex(50)}; + + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); + graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); + graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(4)}); + graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast(5)}); + graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(6)}); + + // WHEN + const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_5); + + // THEN + const graph_path expected_path{ + {vertex_id_1, vertex_id_3, vertex_id_5}, 9}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(DijkstraShortestPathTest, DijkstraCyclicShortestPath) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + const auto vertex_id_4{graph.add_vertex(40)}; + const auto vertex_id_5{graph.add_vertex(50)}; + + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); + graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(3)}); + graph.add_edge(vertex_id_4, vertex_id_2, edge_t{static_cast(4)}); + graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(5)}); + + // WHEN + const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_5); + + // THEN + const graph_path expected_path{ + {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8}; + ASSERT_EQ(path, expected_path); +} + +TYPED_TEST(DijkstraShortestPathSignedTypesTest, DijkstraNegativeWeight) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(-1)}); + + // THEN + ASSERT_THROW( + { + try { + // Call the get_edge function for non-existing vertices + [[maybe_unused]] const auto path{ + dijkstra_shortest_path(graph, vertex_id_1, vertex_id_2)}; + // If the above line doesn't throw an exception, fail the test + FAIL() + << "Expected std::invalid_argument exception, but no exception " + "was thrown."; + } catch (const std::invalid_argument &ex) { + // Verify that the exception message contains the expected error + // message + EXPECT_EQ( + ex.what(), + fmt::format( + "Negative edge weight [{}] between vertices [{}] -> [{}].", + -1, vertex_id_1, vertex_id_2)); + throw; + } + }, + std::invalid_argument); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/shortest_path/dijkstra_shortest_paths_test.cpp b/test/graaflib/algorithm/shortest_path/dijkstra_shortest_paths_test.cpp new file mode 100644 index 00000000..f4b94aa1 --- /dev/null +++ b/test/graaflib/algorithm/shortest_path/dijkstra_shortest_paths_test.cpp @@ -0,0 +1,244 @@ +#include +#include +#include +#include + +namespace graaf::algorithm { + +namespace { + +template +class my_weighted_edge : public weighted_edge { + public: + explicit my_weighted_edge(T weight) : weight_{weight} {} + + [[nodiscard]] T get_weight() const noexcept override { return weight_; } + + private: + T weight_{}; +}; + +template +struct DijkstraShortestPathsTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(DijkstraShortestPathsTest, weighted_graph_types); + +template +struct DijkstraShortestPathsSignedTypesTest : public testing::Test { + using graph_t = typename T::first_type; + using edge_t = typename T::second_type; +}; + +using weighted_graph_signed_types = testing::Types< + + /** + * Primitive edge type directed graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type directed graph + */ + + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(DijkstraShortestPathsSignedTypesTest, + weighted_graph_signed_types); + +} // namespace + +TYPED_TEST(DijkstraShortestPathsTest, DijkstraMinimalShortestPathTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + + // WHEN; + const auto path_map = dijkstra_shortest_paths(graph, vertex_id_1); + + // THEN + const graph_path path1{{vertex_id_1}, 0}; + std::unordered_map> expected_path_map; + expected_path_map[vertex_id_1] = path1; + ASSERT_EQ(path_map, expected_path_map); +} + +TYPED_TEST(DijkstraShortestPathsTest, DijkstraSimpleShortestPathTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(3)}); + + // WHEN + const auto path_map = dijkstra_shortest_paths(graph, vertex_id_1); + + // THEN + const graph_path path1{{vertex_id_1}, 0}; + const graph_path path2{{vertex_id_1, vertex_id_2}, 3}; + + std::unordered_map> expected_path_map; + expected_path_map[vertex_id_1] = path1; + expected_path_map[vertex_id_2] = path2; + ASSERT_EQ(path_map, expected_path_map); +} + +TYPED_TEST(DijkstraShortestPathsTest, DijkstraMoreComplexShortestPathTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + const auto vertex_id_3{graph.add_vertex(30)}; + const auto vertex_id_4{graph.add_vertex(40)}; + const auto vertex_id_5{graph.add_vertex(50)}; + + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(1)}); + graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); + graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(4)}); + graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast(5)}); + graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(6)}); + + // WHEN + const auto path_map = dijkstra_shortest_paths(graph, vertex_id_1); + + // THEN + const graph_path path1{{vertex_id_1}, 0}; + const graph_path path2{{vertex_id_1, vertex_id_2}, 1}; + const graph_path path3{{vertex_id_1, vertex_id_2, vertex_id_3}, 2}; + const graph_path path4{ + {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_4}, 6}; + const graph_path path5{ + {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8}; + + std::unordered_map> expected_path_map; + expected_path_map[vertex_id_1] = path1; + expected_path_map[vertex_id_2] = path2; + expected_path_map[vertex_id_3] = path3; + expected_path_map[vertex_id_4] = path4; + expected_path_map[vertex_id_5] = path5; + ASSERT_EQ(path_map, expected_path_map); +} + +TYPED_TEST(DijkstraShortestPathsSignedTypesTest, DijkstraNegativeWeightTree) { + // GIVEN + using graph_t = typename TestFixture::graph_t; + using edge_t = typename TestFixture::edge_t; + using weight_t = decltype(get_weight(std::declval())); + + graph_t graph{}; + + const auto vertex_id_1{graph.add_vertex(10)}; + const auto vertex_id_2{graph.add_vertex(20)}; + graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(-1)}); + + // THEN + ASSERT_THROW( + { + try { + // Call the get_edge function for non-existing vertices + [[maybe_unused]] const auto path{ + dijkstra_shortest_paths(graph, vertex_id_1)}; + // If the above line doesn't throw an exception, fail the test + FAIL() + << "Expected std::invalid_argument exception, but no exception " + "was thrown."; + } catch (const std::invalid_argument &ex) { + // Verify that the exception message contains the expected error + // message + EXPECT_EQ( + ex.what(), + fmt::format( + "Negative edge weight [{}] between vertices [{}] -> [{}].", + -1, vertex_id_1, vertex_id_2)); + throw; + } + }, + std::invalid_argument); +} + +} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/floyd_warshall_test.cpp b/test/graaflib/algorithm/shortest_path/floyd_warshall_test.cpp similarity index 100% rename from test/graaflib/algorithm/floyd_warshall_test.cpp rename to test/graaflib/algorithm/shortest_path/floyd_warshall_test.cpp diff --git a/test/graaflib/algorithm/shortest_path_test.cpp b/test/graaflib/algorithm/shortest_path_test.cpp deleted file mode 100644 index 1c351443..00000000 --- a/test/graaflib/algorithm/shortest_path_test.cpp +++ /dev/null @@ -1,929 +0,0 @@ -#include -#include -#include -#include -#include - -namespace graaf::algorithm { - -namespace { -template -struct TypedShortestPathTest : public testing::Test { - using graph_t = T; -}; - -using graph_types = - testing::Types, undirected_graph>; -TYPED_TEST_SUITE(TypedShortestPathTest, graph_types); -} // namespace - -TYPED_TEST(TypedShortestPathTest, BfsMinimalShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - graph_t graph{}; - - const auto vertex_1{graph.add_vertex(10)}; - - // WHEN - const auto path = bfs_shortest_path(graph, vertex_1, vertex_1); - - // THEN - const graph_path expected_path{{vertex_1}, 0}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(TypedShortestPathTest, BfsNoAvailablePath) { - // 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)}; - - // WHEN - const auto path = bfs_shortest_path(graph, vertex_1, vertex_2); - - // THEN - ASSERT_FALSE(path.has_value()); -} - -TYPED_TEST(TypedShortestPathTest, BfsSimpleShortestPath) { - // 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)}; - - // We add an edge from the vertex where we start the traversal - // so it does not matter whether this is a directed or undirected graph - graph.add_edge(vertex_1, vertex_2, 100); - - // WHEN - const auto path = bfs_shortest_path(graph, vertex_1, vertex_2); - - // THEN - const graph_path expected_path{{vertex_1, vertex_2}, 1}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(TypedShortestPathTest, BfsMoreComplexShortestPath) { - // 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_2, vertex_3, 200); - graph.add_edge(vertex_1, vertex_3, 300); - graph.add_edge(vertex_3, vertex_4, 400); - graph.add_edge(vertex_4, vertex_5, 500); - graph.add_edge(vertex_3, vertex_5, 600); - - // WHEN - const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); - - // THEN - const graph_path expected_path{{vertex_1, vertex_3, vertex_5}, 2}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(TypedShortestPathTest, BfsCyclicShortestPath) { - // 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_2, vertex_3, 200); - graph.add_edge(vertex_3, vertex_4, 300); - graph.add_edge(vertex_4, vertex_2, 300); - graph.add_edge(vertex_3, vertex_5, 400); - - // WHEN - const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); - - // THEN - const graph_path expected_path{{vertex_1, vertex_2, vertex_3, vertex_5}, - 3}; - ASSERT_EQ(path, expected_path); -} - -TEST(ShortestPathTest, BfsDirectedrWrongDirectionShortestPath) { - // GIVEN - directed_graph 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)}; - - // Edge between 2 and 3 is inverted, so path needs to take detour via 4 - graph.add_edge(vertex_1, vertex_2, 100); - graph.add_edge(vertex_3, vertex_2, 200); - graph.add_edge(vertex_3, vertex_5, 300); - graph.add_edge(vertex_2, vertex_4, 400); - graph.add_edge(vertex_4, vertex_3, 500); - - // WHEN - const auto path = bfs_shortest_path(graph, vertex_1, vertex_5); - - // THEN - const graph_path expected_path{ - {vertex_1, vertex_2, vertex_4, vertex_3, vertex_5}, 4}; - ASSERT_EQ(path, expected_path); -} - -template -class my_weighted_edge : public weighted_edge { - public: - explicit my_weighted_edge(T weight) : weight_{weight} {} - - [[nodiscard]] T get_weight() const noexcept override { return weight_; } - - private: - T weight_{}; -}; - -template -struct DijkstraShortestPathTest : public testing::Test { - using graph_t = typename T::first_type; - using edge_t = typename T::second_type; -}; - -using weighted_graph_types = testing::Types< - - /** - * Primitive edge type directed graph - */ - std::pair, int>, - std::pair, unsigned long>, - std::pair, float>, - std::pair, long double>, - - /** - * Non primitive weighted edge type directed graph - */ - - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - - /** - * Primitive edge type undirected graph - */ - std::pair, int>, - std::pair, unsigned long>, - std::pair, float>, - std::pair, long double>, - - /** - * Non primitive weighted edge type undirected graph - */ - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>>; - -TYPED_TEST_SUITE(DijkstraShortestPathTest, weighted_graph_types); - -TYPED_TEST(DijkstraShortestPathTest, DijkstraMinimalShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - - // WHEN; - const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_1); - - // THEN - const graph_path expected_path{{vertex_id_1}, 0}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraNoAvailablePath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - - // WHEN; - const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_2); - - // THEN - ASSERT_FALSE(path.has_value()); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraSimpleShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(3)}); - - // WHEN - const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_2); - - // THEN - const graph_path expected_path{{vertex_id_1, vertex_id_2}, 3}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraMoreComplexShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - const auto vertex_id_4{graph.add_vertex(40)}; - const auto vertex_id_5{graph.add_vertex(50)}; - - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); - graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); - graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(4)}); - graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast(5)}); - graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(6)}); - - // WHEN - const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_5); - - // THEN - const graph_path expected_path{ - {vertex_id_1, vertex_id_3, vertex_id_5}, 9}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraCyclicShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - const auto vertex_id_4{graph.add_vertex(40)}; - const auto vertex_id_5{graph.add_vertex(50)}; - - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); - graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(3)}); - graph.add_edge(vertex_id_4, vertex_id_2, edge_t{static_cast(4)}); - graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(5)}); - - // WHEN - const auto path = dijkstra_shortest_path(graph, vertex_id_1, vertex_id_5); - - // THEN - const graph_path expected_path{ - {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8}; - ASSERT_EQ(path, expected_path); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraMinimalShortestPathTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - - // WHEN; - const auto path_map = dijkstra_shortest_paths(graph, vertex_id_1); - - // THEN - const graph_path path1{{vertex_id_1}, 0}; - std::unordered_map> expected_path_map; - expected_path_map[vertex_id_1] = path1; - ASSERT_EQ(path_map, expected_path_map); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraSimpleShortestPathTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(3)}); - - // WHEN - const auto path_map = dijkstra_shortest_paths(graph, vertex_id_1); - - // THEN - const graph_path path1{{vertex_id_1}, 0}; - const graph_path path2{{vertex_id_1, vertex_id_2}, 3}; - - std::unordered_map> expected_path_map; - expected_path_map[vertex_id_1] = path1; - expected_path_map[vertex_id_2] = path2; - ASSERT_EQ(path_map, expected_path_map); -} - -TYPED_TEST(DijkstraShortestPathTest, DijkstraMoreComplexShortestPathTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - const auto vertex_id_4{graph.add_vertex(40)}; - const auto vertex_id_5{graph.add_vertex(50)}; - - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); - graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(4)}); - graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast(5)}); - graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(6)}); - - // WHEN - const auto path_map = dijkstra_shortest_paths(graph, vertex_id_1); - - // THEN - const graph_path path1{{vertex_id_1}, 0}; - const graph_path path2{{vertex_id_1, vertex_id_2}, 1}; - const graph_path path3{{vertex_id_1, vertex_id_2, vertex_id_3}, 2}; - const graph_path path4{ - {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_4}, 6}; - const graph_path path5{ - {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8}; - - std::unordered_map> expected_path_map; - expected_path_map[vertex_id_1] = path1; - expected_path_map[vertex_id_2] = path2; - expected_path_map[vertex_id_3] = path3; - expected_path_map[vertex_id_4] = path4; - expected_path_map[vertex_id_5] = path5; - ASSERT_EQ(path_map, expected_path_map); -} - -template -struct DijkstraShortestPathSignedTypesTest : public testing::Test { - using graph_t = typename T::first_type; - using edge_t = typename T::second_type; -}; - -using weighted_graph_signed_types = testing::Types< - - /** - * Primitive edge type directed graph - */ - std::pair, int>, - std::pair, float>, - std::pair, long double>, - - /** - * Non primitive weighted edge type directed graph - */ - - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - - /** - * Primitive edge type undirected graph - */ - std::pair, int>, - std::pair, float>, - std::pair, long double>, - - /** - * Non primitive weighted edge type undirected graph - */ - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>>; - -TYPED_TEST_SUITE(DijkstraShortestPathSignedTypesTest, - weighted_graph_signed_types); - -TYPED_TEST(DijkstraShortestPathSignedTypesTest, DijkstraNegativeWeight) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(-1)}); - - // THEN - ASSERT_THROW( - { - try { - // Call the get_edge function for non-existing vertices - [[maybe_unused]] const auto path{ - dijkstra_shortest_path(graph, vertex_id_1, vertex_id_2)}; - // If the above line doesn't throw an exception, fail the test - FAIL() - << "Expected std::invalid_argument exception, but no exception " - "was thrown."; - } catch (const std::invalid_argument &ex) { - // Verify that the exception message contains the expected error - // message - EXPECT_EQ( - ex.what(), - fmt::format( - "Negative edge weight [{}] between vertices [{}] -> [{}].", - -1, vertex_id_1, vertex_id_2)); - throw; - } - }, - std::invalid_argument); -} - -TYPED_TEST(DijkstraShortestPathSignedTypesTest, DijkstraNegativeWeightTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(-1)}); - - // THEN - ASSERT_THROW( - { - try { - // Call the get_edge function for non-existing vertices - [[maybe_unused]] const auto path{ - dijkstra_shortest_paths(graph, vertex_id_1)}; - // If the above line doesn't throw an exception, fail the test - FAIL() - << "Expected std::invalid_argument exception, but no exception " - "was thrown."; - } catch (const std::invalid_argument &ex) { - // Verify that the exception message contains the expected error - // message - EXPECT_EQ( - ex.what(), - fmt::format( - "Negative edge weight [{}] between vertices [{}] -> [{}].", - -1, vertex_id_1, vertex_id_2)); - throw; - } - }, - std::invalid_argument); -} - -template -struct BellmanFordShortestPathsTest : public testing::Test { - using graph_t = typename T::first_type; - using edge_t = typename T::second_type; -}; - -TYPED_TEST_SUITE(BellmanFordShortestPathsTest, weighted_graph_types); - -TYPED_TEST(BellmanFordShortestPathsTest, BellmanFordMinimalShortestPathsTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - - // WHEN; - const auto path_map = bellman_ford_shortest_paths(graph, vertex_id_1); - - // THEN - const graph_path path1{{vertex_id_1}, 0}; - std::unordered_map> expected_path_map; - expected_path_map[vertex_id_1] = path1; - ASSERT_EQ(path_map, expected_path_map); -} - -TYPED_TEST(BellmanFordShortestPathsTest, BellmanFordShortestPathsTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(3)}); - - // WHEN - const auto path_map = bellman_ford_shortest_paths(graph, vertex_id_1); - - // THEN - const graph_path path1{{vertex_id_1}, 0}; - const graph_path path2{{vertex_id_1, vertex_id_2}, 3}; - - std::unordered_map> expected_path_map; - expected_path_map[vertex_id_1] = path1; - expected_path_map[vertex_id_2] = path2; - ASSERT_EQ(path_map, expected_path_map); -} - -TYPED_TEST(BellmanFordShortestPathsTest, - BellmanFordMoreComplexShortestPathTree) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - const auto vertex_id_4{graph.add_vertex(40)}; - const auto vertex_id_5{graph.add_vertex(50)}; - - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); - graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(4)}); - graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast(5)}); - graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast(6)}); - - // WHEN - const auto path_map = bellman_ford_shortest_paths(graph, vertex_id_1); - - // THEN - const graph_path path1{{vertex_id_1}, 0}; - const graph_path path2{{vertex_id_1, vertex_id_2}, 1}; - const graph_path path3{{vertex_id_1, vertex_id_2, vertex_id_3}, 2}; - const graph_path path4{ - {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_4}, 6}; - const graph_path path5{ - {vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8}; - - std::unordered_map> expected_path_map; - expected_path_map[vertex_id_1] = path1; - expected_path_map[vertex_id_2] = path2; - expected_path_map[vertex_id_3] = path3; - expected_path_map[vertex_id_4] = path4; - expected_path_map[vertex_id_5] = path5; - ASSERT_EQ(path_map, expected_path_map); -} -template -struct BellmanFordShortestPathsSignedTypesTest : public testing::Test { - using graph_t = typename T::first_type; - using edge_t = typename T::second_type; -}; -using directed_weighted_graph_signed_types = testing::Types< - /** - * Primitive edge type directed graph - */ - std::pair, int>, - std::pair, float>, - std::pair, long double>, - - /** - * Non primitive weighted edge type directed graph - */ - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>, - std::pair>, - my_weighted_edge>>; - -TYPED_TEST_SUITE(BellmanFordShortestPathsSignedTypesTest, - directed_weighted_graph_signed_types); - -TYPED_TEST(BellmanFordShortestPathsSignedTypesTest, - BellmanFordNegativecycleTest) { - // Bellman Ford can handle negative edge weights only in directed graphs. - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - // Adding vertices - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - const auto vertex_id_4{graph.add_vertex(40)}; - - // Adding Edges - - // Negative cycle exists between the vertices 2,3 and 4. - graph.add_edge(vertex_id_2, vertex_id_1, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(-6)}); - graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(-1)}); - graph.add_edge(vertex_id_4, vertex_id_2, edge_t{static_cast(-2)}); - - ASSERT_THROW( - { - try { - [[maybe_unused]] const auto path{ - bellman_ford_shortest_paths(graph, vertex_id_3)}; - // If the above line doesn't throw an exception, fail the test - FAIL() << "Expected std::invalid_argument exception, but no " - "exception was thrown"; - } catch (const std::invalid_argument &ex) { - // Verify that the exception message contains the expected err or - // message. - EXPECT_STREQ("Negative cycle detected in the graph.", ex.what()); - throw; - } - }, - std::invalid_argument); -} -template -struct AStarShortestPathTest : public testing::Test { - using graph_t = typename T::first_type; - using edge_t = typename T::second_type; -}; - -TYPED_TEST_SUITE(AStarShortestPathTest, weighted_graph_types); - -// Graph with only one vertex. -TYPED_TEST(AStarShortestPathTest, AStarMinimalShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - - // WHEN - // The weight_t type annotation and static_cast are needed to avoid compiler - // warnings. - const auto heuristic = [](vertex_id_t vertex) -> weight_t { - return static_cast(0); - }; - const auto path = a_star_search(graph, vertex_id_1, vertex_id_1, heuristic); - - // THEN - const graph_path expected_path{{vertex_id_1}, 0}; - ASSERT_EQ(path, expected_path); -} - -// Find the shortest path between the only two vertices in a graph. -TYPED_TEST(AStarShortestPathTest, AStarSimpleShortestPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - - const auto heuristic = [](vertex_id_t vertex) -> weight_t { - return static_cast(0); - }; - - // WHEN - const auto path = a_star_search(graph, vertex_id_1, vertex_id_2, heuristic); - - // THEN - const graph_path expected_path{{vertex_id_1, vertex_id_2}, 1}; - ASSERT_EQ(path, expected_path); -} - -// Graph where there's no path between the start and target vertices. -TYPED_TEST(AStarShortestPathTest, NoPathExistence) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - // Define start and target vertices - vertex_id_t start_vertex = 0; - vertex_id_t target_vertex = 5; - - // Define a heuristic function that always returns 0 - auto zero_heuristic = [](vertex_id_t vertex) -> weight_t { - return static_cast(0); - }; - - // WHEN - auto result = - a_star_search(graph, start_vertex, target_vertex, zero_heuristic); - - // THEN - // Check that the result is an empty optional - ASSERT_FALSE(result.has_value()); -} - -// Find the shortest path between multiple possible paths in a graph. -TYPED_TEST(AStarShortestPathTest, MultiplePathsTest) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(2)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); - - // WHEN - const auto heuristic = [](vertex_id_t vertex) -> weight_t { - return static_cast(0); - }; - const auto path = a_star_search(graph, vertex_id_1, vertex_id_3, heuristic); - - // THEN - const graph_path expected_path{{vertex_id_1, vertex_id_3}, 2}; - ASSERT_EQ(path, expected_path); -} - -// Suboptimal Path Test -TYPED_TEST(AStarShortestPathTest, AStarSuboptimalPath) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(2)}); - graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(4)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(3)}); - - // WHEN - const auto heuristic = [](vertex_id_t vertex) -> weight_t { - return static_cast(10); // Overestimate the remaining cost - }; - const auto path = a_star_search(graph, vertex_id_1, vertex_id_3, heuristic); - - // THEN - ASSERT_TRUE(path.has_value()); // Check if optional has a value - // Note: The path might not be the shortest, but it should still be valid -} - -template -struct AStarShortestPathSignedTypesTest : public testing::Test { - using graph_t = typename T::first_type; - using edge_t = typename T::second_type; -}; - -TYPED_TEST_SUITE(AStarShortestPathSignedTypesTest, weighted_graph_signed_types); - -// Negative Weight Test -TYPED_TEST(AStarShortestPathSignedTypesTest, AStarNegativeWeight) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - graph.add_edge(vertex_id_1, vertex_id_2, - edge_t{static_cast(-1)}); // Negative weight edge - - const auto heuristic = [](vertex_id_t vertex) -> weight_t { - return static_cast(0); - }; - - // THEN - // Taken from DijkstraNegativeWeight - ASSERT_THROW( - { - try { - // Call the get_edge function for non-existing vertices - [[maybe_unused]] const auto path{ - a_star_search(graph, vertex_id_1, vertex_id_2, heuristic)}; - // If the above line doesn't throw an exception, fail the test - FAIL() - << "Expected std::invalid_argument exception, but no exception " - "was thrown."; - } catch (const std::invalid_argument &ex) { - // Verify that the exception message contains the expected error - // message - EXPECT_EQ( - ex.what(), - fmt::format( - "Negative edge weight [{}] between vertices [{}] -> [{}].", - -1, vertex_id_1, vertex_id_2)); - throw; - } - }, - std::invalid_argument); -} - -// Heuristic Impact Test -TYPED_TEST(AStarShortestPathTest, AStarHeuristicImpact) { - // GIVEN - using graph_t = typename TestFixture::graph_t; - using edge_t = typename TestFixture::edge_t; - using weight_t = decltype(get_weight(std::declval())); - - graph_t graph{}; - - const auto vertex_id_1{graph.add_vertex(10)}; - const auto vertex_id_2{graph.add_vertex(20)}; - const auto vertex_id_3{graph.add_vertex(30)}; - const auto vertex_id_4{graph.add_vertex(40)}; - graph.add_edge(vertex_id_1, vertex_id_2, edge_t{static_cast(1)}); - graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast(2)}); - graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast(3)}); - graph.add_edge(vertex_id_1, vertex_id_4, edge_t{static_cast(2)}); - graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast(1)}); - - const auto start_vertex = vertex_id_1; - const auto target_vertex = vertex_id_3; - - // Define two different heuristic functions - const auto heuristic1 = [](vertex_id_t vertex) -> weight_t { - return static_cast(0); - }; // Underestimating heuristic - const auto heuristic2 = [](vertex_id_t vertex) -> weight_t { - return static_cast(10); - }; // Overestimating heuristic - - // WHEN - const auto path_with_underestimating_heuristic = - a_star_search(graph, start_vertex, target_vertex, heuristic1); - const auto path_with_overestimating_heuristic = - a_star_search(graph, start_vertex, target_vertex, heuristic2); - - // THEN - // Verify that the path with the underestimating heuristic is shorter - ASSERT_TRUE(path_with_underestimating_heuristic.has_value()); - ASSERT_TRUE(path_with_overestimating_heuristic.has_value()); - ASSERT_LT(path_with_underestimating_heuristic->total_weight, - path_with_overestimating_heuristic->total_weight); -} - -} // namespace graaf::algorithm \ No newline at end of file diff --git a/test/graaflib/algorithm/strongly_connected_components_test.cpp b/test/graaflib/algorithm/strongly_connected_components/tarjan_test.cpp similarity index 99% rename from test/graaflib/algorithm/strongly_connected_components_test.cpp rename to test/graaflib/algorithm/strongly_connected_components/tarjan_test.cpp index 7c34f4da..fd1fd56a 100644 --- a/test/graaflib/algorithm/strongly_connected_components_test.cpp +++ b/test/graaflib/algorithm/strongly_connected_components/tarjan_test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/test/graaflib/algorithm/topological_sorting_test.cpp b/test/graaflib/algorithm/topological_sorting/dfs_topological_sorting_test.cpp similarity index 94% rename from test/graaflib/algorithm/topological_sorting_test.cpp rename to test/graaflib/algorithm/topological_sorting/dfs_topological_sorting_test.cpp index 642b15d4..0e4d751e 100644 --- a/test/graaflib/algorithm/topological_sorting_test.cpp +++ b/test/graaflib/algorithm/topological_sorting/dfs_topological_sorting_test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -31,7 +31,7 @@ TYPED_TEST(TypedTopologicalSort, ShortGraph) { graph.add_edge(vertex_3, vertex_4, 45); // WHEN; - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); std::vector expected_vertices{vertex_1, vertex_2, vertex_3, vertex_4}; @@ -55,7 +55,7 @@ TYPED_TEST(TypedTopologicalSort, RhombusShapeGraph) { graph.add_edge(vertex_2, vertex_4, 55); // WHEN; - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); std::vector expected_vertices_1{vertex_1, vertex_2, vertex_3, vertex_4}; std::vector expected_vertices_2{vertex_1, vertex_3, vertex_2, @@ -82,7 +82,7 @@ TYPED_TEST(TypedTopologicalSort, CycleGraph) { graph.add_edge(vertex_4, vertex_1, 55); // WHEN; - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); const auto expected_vertices = std::nullopt; // THEN @@ -105,7 +105,7 @@ TYPED_TEST(TypedTopologicalSort, GraphWithParallelEdge) { graph.add_edge(vertex_1, vertex_4, 55); // WHEN - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); const auto expected_vertices = std::nullopt; // THEN @@ -128,7 +128,7 @@ TYPED_TEST(TypedTopologicalSort, SelfLoop) { graph.add_edge(vertex_3, vertex_4, 25); // WHEN; - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); const auto expected_vertices = std::nullopt; // THEN @@ -158,7 +158,7 @@ TYPED_TEST(TypedTopologicalSort, SimpleGraph) { graph.add_edge(vertex_6, vertex_3, 8); // WHEN; - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); std::vector expected_vertices_1{ vertex_1, vertex_4, vertex_2, vertex_6, vertex_5, vertex_3, vertex_7}; std::vector expected_vertices_2{ @@ -208,7 +208,7 @@ TYPED_TEST(TypedTopologicalSort, SixSortResults) { graph.add_edge(vertex_6, vertex_7, 6); // WHEN; - auto sorted_vertices = topological_sort(graph); + auto sorted_vertices = dfs_topological_sort(graph); std::vector expected_vertices_1{ vertex_1, vertex_2, vertex_3, vertex_4, vertex_5, vertex_6, vertex_7}; std::vector expected_vertices_2{