Skip to content

Commit

Permalink
breaking-change: Expose algorith names to interface (bobluppes#50)
Browse files Browse the repository at this point in the history
* expose algorithm names
* use consistent snake case naming
  • Loading branch information
bobluppes authored Aug 5, 2023
1 parent 7bd1094 commit aebb1b3
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 231 deletions.
11 changes: 8 additions & 3 deletions docs/docs/examples/example-basics/shortest-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ sidebar_position: 2
---

# Shortest Path Example
The shortest path algorithm implemented in `graaf::algorithm::get_shortest_path` can be used to compute the shortest path between any two vertices in a graph.

The shortest path algorithm implemented in `graaf::algorithm::get_shortest_path` can be used to compute the shortest
path between any two vertices in a graph.

Consider the following graph:

Expand All @@ -16,15 +18,17 @@ Consider the following graph:
In order to compute the shortest path between *vertex 0* and *vertex 2*, we call:

```c++
const auto maybe_shortest_path{get_shortest_path<edge_strategy::UNWEIGHTED>(graph, start, target)};
const auto maybe_shortest_path{bfs_shortest_path(graph, start, target)};

// Assert that we found a path at all
assert(maybe_shortest_path.has_value());
auto shortest_path{maybe_shortest_path.value()};
```
## Visualizing the shortest path
If we want to visualize the shortest path on the graph, we can create our own vertex and edge writers. These writers then determine the vertex and edge attributes based on whether the vertex or edge is contained in the shortest path.
If we want to visualize the shortest path on the graph, we can create our own vertex and edge writers. These writers
then determine the vertex and edge attributes based on whether the vertex or edge is contained in the shortest path.
First, we create a datastructure of all edges on the shortest path such that we can query it in the edge writer:
Expand Down Expand Up @@ -62,6 +66,7 @@ const auto edge_writer{
return "label=\"\", color=gray, style=dashed";
}};
```

This yields us the following visualization:

<pre>
Expand Down
4 changes: 2 additions & 2 deletions examples/shortest_path/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ graph_with_start_and_target create_graph_with_start_and_target() {
int main() {
const auto [graph, start, target]{create_graph_with_start_and_target()};

const auto maybe_shortest_path{graaf::algorithm::get_shortest_path<
graaf::algorithm::edge_strategy::UNWEIGHTED>(graph, start, target)};
const auto maybe_shortest_path{
graaf::algorithm::bfs_shortest_path(graph, start, target)};
assert(maybe_shortest_path.has_value());
auto shortest_path{maybe_shortest_path.value()};

Expand Down
18 changes: 14 additions & 4 deletions include/graaflib/algorithm/cycle_detection.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@
namespace graaf::algorithm {

/*
* @brief Traverses the graph and checks for cycles.
* @brief Traverses a directed graph and checks for cycles.
*
* @param graph The graph to traverse.
* @param graph The directed graph to traverse.
*/
template <typename V, typename E, graph_type T>
[[nodiscard]] bool has_cycle(const graph<V, E, T> &graph);
template <typename V, typename E>
[[nodiscard]] bool dfs_cycle_detection(
const graph<V, E, graph_type::DIRECTED> &graph);

/*
* @brief Traverses an undirected graph and checks for cycles.
*
* @param graph The undirected graph to traverse.
*/
template <typename V, typename E>
[[nodiscard]] bool dfs_cycle_detection(
const graph<V, E, graph_type::UNDIRECTED> &graph);

} // namespace graaf::algorithm

Expand Down
60 changes: 29 additions & 31 deletions include/graaflib/algorithm/cycle_detection.tpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ namespace detail {

enum class vertex_color { UNVISITED, VISITED, NO_CYCLE };

template <typename V, typename E, graph_type T>
template <typename V, typename E>
bool do_dfs_directed(
const graph<V, E, T>& graph,
const graph<V, E, graph_type::DIRECTED>& graph,
std::unordered_map<vertex_id_t, vertex_color>& colored_vertices,
vertex_id_t current) {
colored_vertices[current] = vertex_color::VISITED;
Expand All @@ -31,9 +31,9 @@ bool do_dfs_directed(
return false;
}

template <typename V, typename E, graph_type T>
template <typename V, typename E>
bool do_dfs_undirected(
const graph<V, E, T>& graph,
const graph<V, E, graph_type::UNDIRECTED>& graph,
std::unordered_map<vertex_id_t, bool>& visited_vertices,
std::unordered_map<vertex_id_t, vertex_id_t>& parent_vertices,
vertex_id_t parent_vertex, vertex_id_t current) {
Expand All @@ -58,39 +58,37 @@ bool do_dfs_undirected(

} // namespace detail

template <typename V, typename E, graph_type T>
bool has_cycle(const graph<V, E, T>& graph) {
if (graph.is_directed()) {
std::unordered_map<vertex_id_t, detail::vertex_color> colored_vertices{};
template <typename V, typename E>
bool dfs_cycle_detection(const graph<V, E, graph_type::DIRECTED>& graph) {
std::unordered_map<vertex_id_t, detail::vertex_color> colored_vertices{};

for (const auto& vertex : graph.get_vertices()) {
using enum detail::vertex_color;
if (colored_vertices[vertex.first] == UNVISITED &&
detail::do_dfs_directed(graph, colored_vertices, vertex.first)) {
return true;
}
for (const auto& vertex : graph.get_vertices()) {
using enum detail::vertex_color;
if (colored_vertices[vertex.first] == UNVISITED &&
detail::do_dfs_directed(graph, colored_vertices, vertex.first)) {
return true;
}

return false;
}

if (graph.is_undirected()) {
// Number of vertices cannot be zero (in case if graph is empty)
if (graph.edge_count() >= graph.vertex_count() &&
graph.vertex_count() > 0) {
return true;
}
return false;
}

std::unordered_map<vertex_id_t, bool> visited_vertices{};
std::unordered_map<vertex_id_t, vertex_id_t> parent_vertices{};
template <typename V, typename E>
bool dfs_cycle_detection(const graph<V, E, graph_type::UNDIRECTED>& graph) {
// Number of vertices cannot be zero (in case if graph is empty)
if (graph.edge_count() >= graph.vertex_count() && graph.vertex_count() > 0) {
return true;
}

for (const auto& vertex : graph.get_vertices()) {
if (!visited_vertices.contains(vertex.first) &&
detail::do_dfs_undirected(graph, visited_vertices, parent_vertices,
vertex.first,
parent_vertices[vertex.first])) {
return true;
}
std::unordered_map<vertex_id_t, bool> visited_vertices{};
std::unordered_map<vertex_id_t, vertex_id_t> parent_vertices{};

for (const auto& vertex : graph.get_vertices()) {
if (!visited_vertices.contains(vertex.first) &&
detail::do_dfs_undirected(graph, visited_vertices, parent_vertices,
vertex.first,
parent_vertices[vertex.first])) {
return true;
}
}

Expand Down
26 changes: 18 additions & 8 deletions include/graaflib/algorithm/graph_traversal.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,34 @@

namespace graaf::algorithm {

enum class search_strategy { DFS, BFS };
/**
* @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 callback A callback which is called for each traversed vertex. Should
* be invocable with a vertex_id_t.
*/
template <typename V, typename E, graph_type T, typename CALLBACK_T>
requires std::invocable<const CALLBACK_T &, vertex_id_t>
void breadth_first_traverse(const graph<V, E, T> &graph,
vertex_id_t start_vertex,
const CALLBACK_T &callback);

/**
* @brief Traverses the graph, starting at start_vertex, and visits all
* reachable vertices.
* reachable vertices in a DFS manner.
*
* @tparam ALGORITHM Tag to specify the search algorithm, can be either DFS of
* BFS.
* @param graph The graph to traverse.
* @param start_vertex Vertex id where the traversal should be started.
* @param callback A callback which is called for each traversed vertex. Should
* be invocable with a vertex_id_t.
*/
template <search_strategy ALGORITHM, typename V, typename E, graph_type T,
typename CALLBACK_T>
template <typename V, typename E, graph_type T, typename CALLBACK_T>
requires std::invocable<const CALLBACK_T &, vertex_id_t>
void traverse(const graph<V, E, T> &graph, vertex_id_t start_vertex,
const CALLBACK_T &callback);
void depth_first_traverse(const graph<V, E, T> &graph, vertex_id_t start_vertex,
const CALLBACK_T &callback);

} // namespace graaf::algorithm

Expand Down
55 changes: 26 additions & 29 deletions include/graaflib/algorithm/graph_traversal.tpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@ namespace graaf::algorithm {

namespace detail {

template <typename V, typename E, graph_type T, typename CALLBACK_T>
void do_dfs(const graph<V, E, T>& graph,
std::unordered_set<vertex_id_t>& seen_vertices, vertex_id_t current,
const CALLBACK_T& callback) {
callback(current);
seen_vertices.insert(current);

for (auto neighbor_vertex : graph.get_neighbors(current)) {
if (!seen_vertices.contains(neighbor_vertex)) {
do_dfs(graph, seen_vertices, neighbor_vertex, callback);
}
}
}

template <typename V, typename E, graph_type T, typename CALLBACK_T>
void do_bfs(const graph<V, E, T>& graph,
std::unordered_set<vertex_id_t>& seen_vertices,
Expand Down Expand Up @@ -49,26 +35,37 @@ void do_bfs(const graph<V, E, T>& graph,
}
}

template <typename V, typename E, graph_type T, typename CALLBACK_T>
void do_dfs(const graph<V, E, T>& graph,
std::unordered_set<vertex_id_t>& seen_vertices, vertex_id_t current,
const CALLBACK_T& callback) {
callback(current);
seen_vertices.insert(current);

for (auto neighbor_vertex : graph.get_neighbors(current)) {
if (!seen_vertices.contains(neighbor_vertex)) {
do_dfs(graph, seen_vertices, neighbor_vertex, callback);
}
}
}

} // namespace detail

template <search_strategy ALGORITHM, typename V, typename E, graph_type T,
typename CALLBACK_T>
template <typename V, typename E, graph_type T, typename CALLBACK_T>
requires std::invocable<const CALLBACK_T&, vertex_id_t>
void traverse(const graph<V, E, T>& graph, vertex_id_t start_vertex,
const CALLBACK_T& callback) {
void breadth_first_traverse(const graph<V, E, T>& graph,
vertex_id_t start_vertex,
const CALLBACK_T& callback) {
std::unordered_set<vertex_id_t> seen_vertices{};
return detail::do_bfs(graph, seen_vertices, start_vertex, callback);
}

using enum search_strategy;
if constexpr (ALGORITHM == DFS) {
return detail::do_dfs(graph, seen_vertices, start_vertex, callback);
}

if constexpr (ALGORITHM == BFS) {
return detail::do_bfs(graph, seen_vertices, start_vertex, callback);
}

// We should never reach this
std::abort();
template <typename V, typename E, graph_type T, typename CALLBACK_T>
requires std::invocable<const CALLBACK_T&, vertex_id_t>
void depth_first_traverse(const graph<V, E, T>& graph, vertex_id_t start_vertex,
const CALLBACK_T& callback) {
std::unordered_set<vertex_id_t> seen_vertices{};
return detail::do_dfs(graph, seen_vertices, start_vertex, callback);
}

} // namespace graaf::algorithm
31 changes: 21 additions & 10 deletions include/graaflib/algorithm/shortest_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,43 @@

namespace graaf::algorithm {

// TODO(bluppes): I would expose the names of the underlying algorithms here.
enum class edge_strategy { WEIGHTED, UNWEIGHTED };

template <typename WEIGHT_T>
struct GraphPath {
struct graph_path {
std::list<vertex_id_t> vertices;
WEIGHT_T total_weight;

bool operator==(const GraphPath& other) const {
bool operator==(const graph_path& other) const {
return vertices == other.vertices && total_weight == other.total_weight;
}
};

/**
* @brief calculates the shortest path between on start_vertex and one
* end_vertex.
* 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.
*/
template <typename V, typename E, graph_type T,
typename WEIGHT_T = decltype(get_weight(std::declval<E>()))>
std::optional<graph_path<WEIGHT_T>> bfs_shortest_path(
const graph<V, E, T>& graph, vertex_id_t start_vertex,
vertex_id_t end_vertex);

/**
* @brief calculates the shortest path between on 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.
*
* @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_type T,
template <typename V, typename E, graph_type T,
typename WEIGHT_T = decltype(get_weight(std::declval<E>()))>
std::optional<GraphPath<WEIGHT_T>> get_shortest_path(
std::optional<graph_path<WEIGHT_T>> dijkstra_shortest_path(
const graph<V, E, T>& graph, vertex_id_t start_vertex,
vertex_id_t end_vertex);

Expand Down
Loading

0 comments on commit aebb1b3

Please sign in to comment.