-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Unweighted shortest path (#18)
* Calc shortest path for unweighted graphs using BFS
- Loading branch information
Showing
3 changed files
with
282 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#pragma once | ||
|
||
#include <graaflib/graph.h> | ||
#include <graaflib/types.h> | ||
|
||
#include <concepts> | ||
#include <list> | ||
#include <optional> | ||
|
||
namespace graaf::algorithm { | ||
|
||
enum class edge_strategy { WEIGHTED, UNWEIGHTED }; | ||
|
||
template <typename E> | ||
struct GraphPath { | ||
std::list<vertex_id_t> vertices; | ||
E total_weight; | ||
|
||
bool operator==(const GraphPath& other) const { | ||
return vertices == other.vertices && total_weight == other.total_weight; | ||
} | ||
}; | ||
|
||
/** | ||
* @brief calculates the shortest path between on start_vertex and one | ||
* end_vertex. | ||
* | ||
* @tparam EDGE_STRATEGY Tag to specify how to handle edges, can be either | ||
* WEIGHTED or UNWEIGHTED. | ||
* @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. | ||
*/ | ||
template <edge_strategy EDGE_STRATEGY, typename V, typename E, graph_spec S> | ||
std::optional<GraphPath<E>> get_shortest_path(const graph<V, E, S>& graph, | ||
vertex_id_t start_vertex, | ||
vertex_id_t end_vertex); | ||
|
||
} // namespace graaf::algorithm | ||
|
||
#include "shortest_path.tpp" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
#pragma once | ||
|
||
#include <algorithm> | ||
#include <optional> | ||
#include <queue> | ||
#include <unordered_map> | ||
#include <unordered_set> | ||
|
||
namespace graaf::algorithm { | ||
|
||
namespace detail { | ||
|
||
template <typename V, typename E, graph_spec S> | ||
std::optional<GraphPath<int>> get_unweighted_shortest_path( | ||
const graph<V, E, S>& graph, vertex_id_t start_vertex, | ||
vertex_id_t end_vertex) { | ||
std::unordered_set<vertex_id_t> seen_vertices{}; | ||
std::unordered_map<vertex_id_t, vertex_id_t> prev_vertex{}; | ||
std::queue<vertex_id_t> to_explore{}; | ||
|
||
to_explore.push(start_vertex); | ||
seen_vertices.insert(start_vertex); | ||
|
||
// TODO: align/merge with implementation of do_bfs in graph_traversal.tpp | ||
while (!to_explore.empty()) { | ||
auto current{to_explore.front()}; | ||
to_explore.pop(); | ||
|
||
if (current == end_vertex) { | ||
break; | ||
} | ||
|
||
for (const auto& neighbor : graph.get_neighbors(current)) { | ||
if (!seen_vertices.contains(neighbor)) { | ||
seen_vertices.insert(neighbor); | ||
prev_vertex[neighbor] = current; | ||
to_explore.push(neighbor); | ||
} | ||
} | ||
} | ||
|
||
const auto reconstruct_path = [&start_vertex, &end_vertex, &prev_vertex]() { | ||
GraphPath<int> path; | ||
auto current{end_vertex}; | ||
|
||
while (current != start_vertex) { | ||
path.vertices.push_front(current); | ||
current = prev_vertex[current]; | ||
} | ||
|
||
path.vertices.push_front(start_vertex); | ||
path.total_weight = path.vertices.size(); | ||
|
||
return path; | ||
}; | ||
|
||
if (seen_vertices.contains(end_vertex)) { | ||
return reconstruct_path(); | ||
} else { | ||
return std::nullopt; | ||
} | ||
} | ||
|
||
} // namespace detail | ||
|
||
template <edge_strategy EDGE_STRATEGY, typename V, typename E, graph_spec S> | ||
std::optional<GraphPath<E>> get_shortest_path(const graph<V, E, S>& graph, | ||
vertex_id_t start_vertex, | ||
vertex_id_t end_vertex) { | ||
using enum edge_strategy; | ||
if constexpr (EDGE_STRATEGY == UNWEIGHTED) { | ||
return detail::get_unweighted_shortest_path(graph, start_vertex, | ||
end_vertex); | ||
} | ||
|
||
if constexpr (EDGE_STRATEGY == WEIGHTED) { | ||
// TODO: Implement A* or Dijkstra | ||
throw std::logic_error( | ||
"Shortest path for weighted graphs not yet implemented."); | ||
} | ||
} | ||
|
||
} // namespace graaf::algorithm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
#include <graaflib/algorithm/shortest_path.h> | ||
#include <graaflib/directed_graph.h> | ||
#include <graaflib/types.h> | ||
#include <graaflib/undirected_graph.h> | ||
#include <gtest/gtest.h> | ||
|
||
namespace graaf::algorithm { | ||
|
||
namespace { | ||
template <typename T> | ||
struct TypedShortestPathTest : public testing::Test { | ||
using graph_t = T; | ||
}; | ||
|
||
using graph_types = | ||
testing::Types<directed_graph<int, int>, undirected_graph<int, int>>; | ||
TYPED_TEST_CASE(TypedShortestPathTest, graph_types); | ||
} // namespace | ||
|
||
TYPED_TEST(TypedShortestPathTest, UnweightedMinimalShortestPath) { | ||
// GIVEN | ||
using graph_t = typename TestFixture::graph_t; | ||
graph_t graph{}; | ||
|
||
const auto vertex_1{graph.add_vertex(10)}; | ||
|
||
// WHEN | ||
const auto path = | ||
get_shortest_path<edge_strategy::UNWEIGHTED>(graph, vertex_1, vertex_1); | ||
|
||
// THEN | ||
const GraphPath<int> expected_path{{vertex_1}, 1}; | ||
ASSERT_EQ(path, expected_path); | ||
} | ||
|
||
TYPED_TEST(TypedShortestPathTest, UnweightedNoAvailablePath) { | ||
// 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 = | ||
get_shortest_path<edge_strategy::UNWEIGHTED>(graph, vertex_1, vertex_2); | ||
|
||
// THEN | ||
ASSERT_FALSE(path.has_value()); | ||
} | ||
|
||
TYPED_TEST(TypedShortestPathTest, UnweightedSimpleShortestPath) { | ||
// 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 = | ||
get_shortest_path<edge_strategy::UNWEIGHTED>(graph, vertex_1, vertex_2); | ||
|
||
// THEN | ||
const GraphPath<int> expected_path{{vertex_1, vertex_2}, 2}; | ||
ASSERT_EQ(path, expected_path); | ||
} | ||
|
||
TYPED_TEST(TypedShortestPathTest, UnweightedMoreComplexShortestPath) { | ||
// 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 = | ||
get_shortest_path<edge_strategy::UNWEIGHTED>(graph, vertex_1, vertex_5); | ||
|
||
// THEN | ||
const GraphPath<int> expected_path{{vertex_1, vertex_3, vertex_5}, 3}; | ||
ASSERT_EQ(path, expected_path); | ||
} | ||
|
||
TYPED_TEST(TypedShortestPathTest, UnweightedCyclicShortestPath) { | ||
// 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 = | ||
get_shortest_path<edge_strategy::UNWEIGHTED>(graph, vertex_1, vertex_5); | ||
|
||
// THEN | ||
const GraphPath<int> expected_path{{vertex_1, vertex_2, vertex_3, vertex_5}, | ||
4}; | ||
ASSERT_EQ(path, expected_path); | ||
} | ||
|
||
TEST(ShortestPathTest, UnweightedDirectedrWongDirectionShortestPath) { | ||
// GIVEN | ||
directed_graph<int, int> 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 = | ||
get_shortest_path<edge_strategy::UNWEIGHTED>(graph, vertex_1, vertex_5); | ||
|
||
// THEN | ||
const GraphPath<int> expected_path{ | ||
{vertex_1, vertex_2, vertex_4, vertex_3, vertex_5}, 5}; | ||
ASSERT_EQ(path, expected_path); | ||
} | ||
|
||
} // namespace graaf::algorithm |