Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Shortest path for weighted graph #38

Merged
merged 9 commits into from
Jul 25, 2023
10 changes: 6 additions & 4 deletions include/graaflib/algorithm/shortest_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ struct GraphPath {
* @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);
template <
edge_strategy EDGE_STRATEGY, typename V, typename E, graph_spec S,
typename WEIGHT_T = typename graph<V, E, S>::edge_t::element_type::weight_t>
std::optional<GraphPath<WEIGHT_T>> get_shortest_path(
const graph<V, E, S>& graph, vertex_id_t start_vertex,
vertex_id_t end_vertex);

} // namespace graaf::algorithm

Expand Down
87 changes: 76 additions & 11 deletions include/graaflib/algorithm/shortest_path.tpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ namespace graaf::algorithm {

namespace detail {

template <typename V, typename E, graph_spec S>
std::optional<GraphPath<int>> get_unweighted_shortest_path(
template <typename V, typename E, graph_spec S, typename WEIGHT_T>
std::optional<GraphPath<WEIGHT_T>> 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{};
Expand Down Expand Up @@ -61,22 +61,87 @@ std::optional<GraphPath<int>> get_unweighted_shortest_path(
}
}

template <typename V, typename E, graph_spec S, typename WEIGHT_T>
std::optional<GraphPath<WEIGHT_T>> get_weighted_shortest_path(
const graph<V, E, S>& graph, vertex_id_t start_vertex,
vertex_id_t end_vertex) {
struct DijkstraVertex {
vertex_id_t id;
WEIGHT_T distance;
vertex_id_t previous;
};

std::unordered_map<vertex_id_t, DijkstraVertex> vertex_info;
std::priority_queue<
DijkstraVertex, std::vector<DijkstraVertex>,
std::function<bool(const DijkstraVertex&, const DijkstraVertex&)>>
to_explore([](const DijkstraVertex& v1, const DijkstraVertex& v2) {
return v1.distance > v2.distance;
});

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 distance =
current.distance + graph.get_edge(current.id, neighbor)->get_weight();

if (!vertex_info.contains(neighbor) ||
distance < vertex_info[neighbor].distance) {
vertex_info[neighbor] = {neighbor, distance, current.id};
to_explore.push(vertex_info[neighbor]);
}
}
}

const auto reconstruct_path = [&start_vertex, &end_vertex, &vertex_info]() {
GraphPath<WEIGHT_T> path;
auto current = end_vertex;

while (current != start_vertex) {
path.vertices.push_back(current);
current = vertex_info[current].previous;
}

path.vertices.push_back(start_vertex);
path.total_weight = vertex_info[end_vertex].distance;

std::reverse(path.vertices.begin(), path.vertices.end());

return path;
};

if (vertex_info.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) {
template <edge_strategy EDGE_STRATEGY, typename V, typename E, graph_spec S,
typename WEIGHT_T>
std::optional<GraphPath<WEIGHT_T>> 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);
return detail::get_unweighted_shortest_path<V, E, S, WEIGHT_T>(
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.");
return detail::get_weighted_shortest_path<V, E, S, WEIGHT_T>(
graph, start_vertex, end_vertex);
}
}

Expand Down
196 changes: 195 additions & 1 deletion test/graaflib/algorithm/shortest_path_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ TYPED_TEST(TypedShortestPathTest, UnweightedCyclicShortestPath) {
ASSERT_EQ(path, expected_path);
}

TEST(ShortestPathTest, UnweightedDirectedrWongDirectionShortestPath) {
TEST(ShortestPathTest, UnweightedDirectedrWrongDirectionShortestPath) {
// GIVEN
directed_graph<int, int> graph{};

Expand All @@ -155,4 +155,198 @@ TEST(ShortestPathTest, UnweightedDirectedrWongDirectionShortestPath) {
ASSERT_EQ(path, expected_path);
}

template <typename T>
class my_weighted_edge : public weighted_edge<T> {
public:
explicit my_weighted_edge(T weight) : weight_{weight} {}

[[nodiscard]] T get_weight() const noexcept override { return weight_; }

private:
T weight_{};
};

template <typename T>
struct WeightedShortestPathTest : 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<directed_graph<int, int>, int>,
std::pair<directed_graph<int, unsigned long>, unsigned long>,
std::pair<directed_graph<int, float>, float>,
std::pair<directed_graph<int, long double>, long double>,

/**
* Non primitive weighted edge type directed graph
*/

std::pair<directed_graph<int, my_weighted_edge<int>>,
my_weighted_edge<int>>,
std::pair<directed_graph<int, my_weighted_edge<unsigned long>>,
my_weighted_edge<unsigned long>>,
std::pair<directed_graph<int, my_weighted_edge<float>>,
my_weighted_edge<float>>,
std::pair<directed_graph<int, my_weighted_edge<long double>>,
my_weighted_edge<long double>>,

/**
* Primitive edge type undirected graph
*/
std::pair<undirected_graph<int, int>, int>,
std::pair<undirected_graph<int, unsigned long>, unsigned long>,
std::pair<undirected_graph<int, float>, float>,
std::pair<undirected_graph<int, long double>, long double>,

/**
* Non primitive weighted edge type undirected graph
*/
std::pair<undirected_graph<int, my_weighted_edge<int>>,
my_weighted_edge<int>>,
std::pair<undirected_graph<int, my_weighted_edge<unsigned long>>,
my_weighted_edge<unsigned long>>,
std::pair<undirected_graph<int, my_weighted_edge<float>>,
my_weighted_edge<float>>,
std::pair<undirected_graph<int, my_weighted_edge<long double>>,
my_weighted_edge<long double>>>;

TYPED_TEST_SUITE(WeightedShortestPathTest, weighted_graph_types);

// Type traits to extract the weight type from both classes and primitives
template <typename T>
struct extract_weight {
using type = T;
};

template <typename T>
requires requires { typename T::weight_t; }
struct extract_weight<T> {
using type = typename T::weight_t;
};

TYPED_TEST(WeightedShortestPathTest, WeightedMinimalShortestPath) {
// GIVEN
using graph_t = typename TestFixture::graph_t;
using edge_t = typename TestFixture::edge_t;
using weight_t = typename extract_weight<edge_t>::type;

graph_t graph{};

const auto vertex_id_1{graph.add_vertex(10)};

// WHEN;
const auto path = get_shortest_path<edge_strategy::WEIGHTED>(
graph, vertex_id_1, vertex_id_1);

// THEN
const GraphPath<weight_t> expected_path{{vertex_id_1}, 0};
ASSERT_EQ(path, expected_path);
}

TYPED_TEST(WeightedShortestPathTest, WeightedNoAvailablePath) {
// GIVEN
using graph_t = typename TestFixture::graph_t;
using edge_t = typename TestFixture::edge_t;
using weight_t = typename extract_weight<edge_t>::type;

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 = get_shortest_path<edge_strategy::WEIGHTED>(
graph, vertex_id_1, vertex_id_2);

// THEN
ASSERT_FALSE(path.has_value());
}

TYPED_TEST(WeightedShortestPathTest, WeightedSimpleShortestPath) {
// GIVEN
using graph_t = typename TestFixture::graph_t;
using edge_t = typename TestFixture::edge_t;
using weight_t = typename extract_weight<edge_t>::type;

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<weight_t>(3)});

// WHEN
const auto path = get_shortest_path<edge_strategy::WEIGHTED>(
graph, vertex_id_1, vertex_id_2);

// THEN
const GraphPath<weight_t> expected_path{{vertex_id_1, vertex_id_2}, 3};
ASSERT_EQ(path, expected_path);
}

TYPED_TEST(WeightedShortestPathTest, WeightedMoreComplexShortestPath) {
// GIVEN
using graph_t = typename TestFixture::graph_t;
using edge_t = typename TestFixture::edge_t;
using weight_t = typename extract_weight<edge_t>::type;

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<weight_t>(1)});
graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast<weight_t>(2)});
graph.add_edge(vertex_id_1, vertex_id_3, edge_t{static_cast<weight_t>(3)});
graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast<weight_t>(4)});
graph.add_edge(vertex_id_4, vertex_id_5, edge_t{static_cast<weight_t>(5)});
graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast<weight_t>(6)});

// WHEN
const auto path = get_shortest_path<edge_strategy::WEIGHTED>(
graph, vertex_id_1, vertex_id_5);

// THEN
const GraphPath<weight_t> expected_path{
{vertex_id_1, vertex_id_3, vertex_id_5}, 9};
ASSERT_EQ(path, expected_path);
}

TYPED_TEST(WeightedShortestPathTest, WeightedCyclicShortestPath) {
// GIVEN
using graph_t = typename TestFixture::graph_t;
using edge_t = typename TestFixture::edge_t;
using weight_t = typename extract_weight<edge_t>::type;

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<weight_t>(1)});
graph.add_edge(vertex_id_2, vertex_id_3, edge_t{static_cast<weight_t>(2)});
graph.add_edge(vertex_id_3, vertex_id_4, edge_t{static_cast<weight_t>(3)});
graph.add_edge(vertex_id_4, vertex_id_2, edge_t{static_cast<weight_t>(4)});
graph.add_edge(vertex_id_3, vertex_id_5, edge_t{static_cast<weight_t>(5)});

// WHEN
const auto path = get_shortest_path<edge_strategy::WEIGHTED>(
graph, vertex_id_1, vertex_id_5);

// THEN
const GraphPath<weight_t> expected_path{
{vertex_id_1, vertex_id_2, vertex_id_3, vertex_id_5}, 8};
ASSERT_EQ(path, expected_path);
}

} // namespace graaf::algorithm