Skip to content

Commit

Permalink
feat: add topological sorting (DFS-based) (bobluppes#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hromz authored Sep 12, 2023
1 parent f6d9aa4 commit e083faa
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ take a look at the [docs](https://bobluppes.github.io/graaf/docs/algorithms/intr
5. [**Strongly Connected Components Algorithms
**](https://bobluppes.github.io/graaf/docs/category/strongly-connected-components):
- Tarjan's Strongly Connected Components
6. [**Topological Sorting Algorithms**](https://bobluppes.github.io/graaf/docs/category/topological-sorting):
- Topological sorting DFS-based

# Contributing

Expand Down
21 changes: 21 additions & 0 deletions docs/docs/algorithms/topological-sort/topological-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
sidebar_position: 1
---

# Topological sort algorithm
Topological sort algorithm processing DAG(directed acyclic graph) using DFS traversal.
Each vertex is visited only after all its dependencies are visited.
The runtime of the algorithm is `O(|V|+|E|)` and the memory consumption is `O(|V|)`.

[wikipedia](https://en.wikipedia.org/wiki/Topological_sorting)

## Syntax

```cpp
template <typename V, typename E>
[[nodiscard]] std::optional<std::vector<vertex_id_t>> topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph);
```
- **graph** The directed graph to traverse.
- **return** Vector of vertices sorted in topological order. If the graph contains cycles, it returns std::nullopt.
22 changes: 22 additions & 0 deletions include/graaflib/algorithm/topological_sorting.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <graaflib/graph.h>

#include <optional>

namespace graaf::algorithm {
/**
* @brief Calculates order of vertices in topological order
* using DFS traversal
*
* @tparam V The vertex type of the graph.
* @tparam E The edge type of the graph.
* @param graph The input graph.
* @return Vector of vertices sorted in topological order
*/
template <typename V, typename E>
[[nodiscard]] std::optional<std::vector<vertex_id_t>> topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph);

} // namespace graaf::algorithm
#include "topological_sorting.tpp"
58 changes: 58 additions & 0 deletions include/graaflib/algorithm/topological_sorting.tpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include <graaflib/algorithm/cycle_detection.h>
#include <graaflib/algorithm/topological_sorting.h>

#include <vector>
#include <optional>
#include <unordered_set>

namespace graaf::algorithm {

namespace detail {

// DFS topological sort
template <typename V, typename E>
void do_dfs_topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph,
vertex_id_t start_vertex,
std::unordered_set<vertex_id_t>& processed_vertices,
std::vector<vertex_id_t>& sorted_vertices) {

processed_vertices.insert(start_vertex);
for (const auto& next_vertex : graph.get_neighbors(start_vertex)) {
if (!processed_vertices.contains(next_vertex)) {
do_dfs_topological_sort(graph, next_vertex, processed_vertices,
sorted_vertices);
}
}

sorted_vertices.push_back(start_vertex);
}

}; // namespace detail

template <typename V, typename E>
std::optional<std::vector<vertex_id_t>> topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph) {

// Graph should be acyclic
if (dfs_cycle_detection(graph)) {
return std::nullopt;
}

std::vector<vertex_id_t> sorted_vertices{};
sorted_vertices.reserve(graph.vertex_count());
std::unordered_set<vertex_id_t> processed_vertices{};

for (const auto& [vertex_id, _] : graph.get_vertices()) {
if (!processed_vertices.contains(vertex_id)) {
detail::do_dfs_topological_sort(graph, vertex_id, processed_vertices, sorted_vertices);
}
}

std::reverse(sorted_vertices.begin(), sorted_vertices.end());
return sorted_vertices;
}

}; // namespace graaf::algorithm
234 changes: 234 additions & 0 deletions test/graaflib/algorithm/topological_sorting_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
#include <fmt/core.h>
#include <graaflib/algorithm/topological_sorting.h>
#include <graaflib/graph.h>
#include <graaflib/types.h>
#include <gtest/gtest.h>

namespace graaf::algorithm {
namespace {
template <typename T>
struct TypedTopologicalSort : public testing::Test {
using graph_t = T;
};

using graph_types = testing::Types<directed_graph<int, int>>;
TYPED_TEST_SUITE(TypedTopologicalSort, graph_types);

}; // namespace

TYPED_TEST(TypedTopologicalSort, ShortGraph) {
// 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)};

graph.add_edge(vertex_1, vertex_2, 25);
graph.add_edge(vertex_2, vertex_3, 35);
graph.add_edge(vertex_3, vertex_4, 45);

// WHEN;
auto sorted_vertices = topological_sort(graph);
std::vector<vertex_id_t> expected_vertices{vertex_1, vertex_2, vertex_3,
vertex_4};

// THEN
ASSERT_EQ(expected_vertices, sorted_vertices);
}

TYPED_TEST(TypedTopologicalSort, RhombusShapeGraph) {
// 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)};

graph.add_edge(vertex_1, vertex_2, 25);
graph.add_edge(vertex_1, vertex_3, 35);
graph.add_edge(vertex_3, vertex_4, 45);
graph.add_edge(vertex_2, vertex_4, 55);

// WHEN;
auto sorted_vertices = topological_sort(graph);
std::vector<vertex_id_t> expected_vertices_1{vertex_1, vertex_2, vertex_3,
vertex_4};
std::vector<vertex_id_t> expected_vertices_2{vertex_1, vertex_3, vertex_2,
vertex_4};

// THEN
ASSERT_TRUE(expected_vertices_1 == sorted_vertices ||
expected_vertices_2 == sorted_vertices);
}

TYPED_TEST(TypedTopologicalSort, CycleGraph) {
// 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)};

graph.add_edge(vertex_1, vertex_2, 25);
graph.add_edge(vertex_2, vertex_3, 35);
graph.add_edge(vertex_3, vertex_4, 45);
graph.add_edge(vertex_4, vertex_1, 55);

// WHEN;
auto sorted_vertices = topological_sort(graph);
const auto expected_vertices = std::nullopt;

// THEN
ASSERT_EQ(expected_vertices, sorted_vertices);
}

TYPED_TEST(TypedTopologicalSort, GraphWithParallelEdge) {
// 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)};

graph.add_edge(vertex_1, vertex_2, 25);
graph.add_edge(vertex_3, vertex_4, 35);
graph.add_edge(vertex_4, vertex_1, 45);
graph.add_edge(vertex_1, vertex_4, 55);

// WHEN
auto sorted_vertices = topological_sort(graph);
const auto expected_vertices = std::nullopt;

// THEN
ASSERT_EQ(expected_vertices, sorted_vertices);
}

TYPED_TEST(TypedTopologicalSort, SelfLoop) {
// 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)};

graph.add_edge(vertex_1, vertex_1, -1);
graph.add_edge(vertex_2, vertex_2, -1);
graph.add_edge(vertex_1, vertex_2, 15);
graph.add_edge(vertex_3, vertex_4, 25);

// WHEN;
auto sorted_vertices = topological_sort(graph);
const auto expected_vertices = std::nullopt;

// THEN
ASSERT_EQ(expected_vertices, sorted_vertices);
}

TYPED_TEST(TypedTopologicalSort, SimpleGraph) {
// 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)};
const auto vertex_6{graph.add_vertex(60)};
const auto vertex_7{graph.add_vertex(70)};

graph.add_edge(vertex_1, vertex_5, 1);
graph.add_edge(vertex_5, vertex_3, 2);
graph.add_edge(vertex_3, vertex_7, 3);
graph.add_edge(vertex_1, vertex_4, 4);
graph.add_edge(vertex_1, vertex_2, 5);
graph.add_edge(vertex_4, vertex_2, 6);
graph.add_edge(vertex_2, vertex_6, 7);
graph.add_edge(vertex_6, vertex_3, 8);

// WHEN;
auto sorted_vertices = topological_sort(graph);
std::vector<vertex_id_t> expected_vertices_1{
vertex_1, vertex_4, vertex_2, vertex_6, vertex_5, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_2{
vertex_1, vertex_2, vertex_4, vertex_6, vertex_5, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_3{
vertex_1, vertex_2, vertex_4, vertex_5, vertex_6, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_4{
vertex_1, vertex_5, vertex_4, vertex_2, vertex_6, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_5{
vertex_1, vertex_5, vertex_2, vertex_4, vertex_6, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_6{
vertex_1, vertex_2, vertex_5, vertex_4, vertex_6, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_7{
vertex_1, vertex_4, vertex_2, vertex_5, vertex_6, vertex_3, vertex_7};
std::vector<vertex_id_t> expected_vertices_8{
vertex_1, vertex_4, vertex_5, vertex_2, vertex_6, vertex_3, vertex_7};

// THEN
ASSERT_TRUE((expected_vertices_1 == sorted_vertices) ||
(expected_vertices_2 == sorted_vertices) ||
(expected_vertices_3 == sorted_vertices) ||
(expected_vertices_4 == sorted_vertices) ||
(expected_vertices_5 == sorted_vertices) ||
(expected_vertices_6 == sorted_vertices) ||
(expected_vertices_7 == sorted_vertices) ||
(expected_vertices_8 == sorted_vertices));
}

TYPED_TEST(TypedTopologicalSort, SixSortResults) {
// 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)};
const auto vertex_6{graph.add_vertex(60)};
const auto vertex_7{graph.add_vertex(70)};

graph.add_edge(vertex_1, vertex_2, 1);
graph.add_edge(vertex_2, vertex_3, 2);
graph.add_edge(vertex_1, vertex_4, 3);
graph.add_edge(vertex_4, vertex_5, 4);
graph.add_edge(vertex_1, vertex_6, 5);
graph.add_edge(vertex_6, vertex_7, 6);

// WHEN;
auto sorted_vertices = topological_sort(graph);
std::vector<vertex_id_t> expected_vertices_1{
vertex_1, vertex_2, vertex_3, vertex_4, vertex_5, vertex_6, vertex_7};
std::vector<vertex_id_t> expected_vertices_2{
vertex_1, vertex_2, vertex_3, vertex_6, vertex_7, vertex_4, vertex_5};
std::vector<vertex_id_t> expected_vertices_3{
vertex_1, vertex_6, vertex_7, vertex_2, vertex_3, vertex_4, vertex_5};
std::vector<vertex_id_t> expected_vertices_4{
vertex_1, vertex_6, vertex_7, vertex_4, vertex_5, vertex_2, vertex_3};
std::vector<vertex_id_t> expected_vertices_5{
vertex_1, vertex_4, vertex_5, vertex_6, vertex_7, vertex_2, vertex_3};
std::vector<vertex_id_t> expected_vertices_6{
vertex_1, vertex_4, vertex_5, vertex_2, vertex_3, vertex_6, vertex_7};

// THEN
ASSERT_TRUE((expected_vertices_1 == sorted_vertices) ||
(expected_vertices_2 == sorted_vertices) ||
(expected_vertices_3 == sorted_vertices) ||
(expected_vertices_4 == sorted_vertices) ||
(expected_vertices_4 == sorted_vertices) ||
(expected_vertices_6 == sorted_vertices));
}

}; // namespace graaf::algorithm

0 comments on commit e083faa

Please sign in to comment.