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: Unweighted shortest path #18

Merged
merged 7 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/graaflib/algorithm/shortest_path.h
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"
83 changes: 83 additions & 0 deletions src/graaflib/algorithm/shortest_path.tpp
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.");
}
bobluppes marked this conversation as resolved.
Show resolved Hide resolved
}

} // namespace graaf::algorithm
158 changes: 158 additions & 0 deletions test/graaflib/algorithm/shortest_path_test.cpp
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