Skip to content

Commit

Permalink
feat: Unweighted shortest path (#18)
Browse files Browse the repository at this point in the history
* Calc shortest path for unweighted graphs using BFS
  • Loading branch information
joweich authored May 29, 2023
1 parent 5b3a566 commit 1764475
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 0 deletions.
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.");
}
}

} // 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

0 comments on commit 1764475

Please sign in to comment.