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

Topological Sorting #93

Merged
merged 39 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3fe9e21
- added DFS cycle detection for directed and undirected graph
Hromz Jul 24, 2023
a629352
- corrected code according to comments
Hromz Jul 25, 2023
99f77cd
Apply suggestions from code review
bobluppes Jul 25, 2023
956eac3
Merge branch 'bobluppes:main' into main
Hromz Jul 28, 2023
3ec396d
Merge branch 'bobluppes:main' into main
Hromz Jul 29, 2023
3ba348d
Merge branch 'bobluppes:main' into main
Hromz Aug 1, 2023
432b6b4
- First example of potential use of the library
Hromz Aug 1, 2023
97ccea7
- small fixes
Hromz Aug 1, 2023
3e55e23
Update transport-example.md
Hromz Aug 2, 2023
286e9ce
Merge remote-tracking branch 'upstream/main'
bobluppes Aug 5, 2023
419b663
clang format fix
bobluppes Aug 5, 2023
2408835
Merge remote-tracking branch 'upstream/main'
bobluppes Aug 5, 2023
7e459bf
reflect breaking changes on main
bobluppes Aug 5, 2023
6fc3af0
clang format fix
bobluppes Aug 5, 2023
767a532
- corrected according to git comments
Hromz Aug 12, 2023
8ebb5f4
small fixes
Hromz Aug 12, 2023
ffe4553
small fixes
Hromz Aug 12, 2023
bb28f8c
Merge remote-tracking branch 'upstream/main'
bobluppes Aug 13, 2023
ce5df25
fix issues after merge
bobluppes Aug 13, 2023
1c82f04
fix syntax highlighting code blocks
bobluppes Aug 13, 2023
1c644a0
clang format
bobluppes Aug 13, 2023
a0fde77
Merge branch 'bobluppes:main' into main
Hromz Aug 13, 2023
2ae7d97
Merge branch 'bobluppes:main' into main
Hromz Aug 13, 2023
a3a3b31
Merge branch 'bobluppes:main' into main
Hromz Aug 15, 2023
c05cd27
- Added MST algorithm
Hromz Aug 18, 2023
b5db653
clang style correction
Hromz Aug 18, 2023
057ee77
Apply suggestions from code review
bobluppes Aug 31, 2023
9e92f3f
Merge remote-tracking branch 'upstream/main'
bobluppes Aug 31, 2023
665a484
Merge branch 'bobluppes:main' into main
Hromz Sep 1, 2023
5dce08b
Added topological sort algorithm DFS-based
Hromz Sep 5, 2023
03200f7
Merge branch 'main' into main
Hromz Sep 5, 2023
f8cd4fb
small fixes
Hromz Sep 5, 2023
21cde40
Merge branch 'main' of https://github.com/Hromz/graaf
Hromz Sep 5, 2023
14f0b8d
clang format fix
Hromz Sep 5, 2023
7efd134
added test
Hromz Sep 5, 2023
0bcad82
more tests
Hromz Sep 5, 2023
f96849a
Minor fixes
Hromz Sep 10, 2023
57d3c47
Clang format
Hromz Sep 10, 2023
c5ddba1
Update topological-sort.md
Hromz Sep 10, 2023
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
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::vector<edge_id_t> kruskal_minimum_spanning_tree(
const graph<V, E, graph_type::UNDIRECTED>& graph);
```

- **graph** The graph to traverse.
- **return** Vector of vertices sorted in topological order. If the graph contains cycles, it returns an empty vector.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **return** Vector of vertices sorted in topological order. If the graph contains cycles, it returns an empty vector.
- **return** Vector of vertices sorted in topological order. If the graph contains cycles, it returns std::nullopt.

20 changes: 20 additions & 0 deletions include/graaflib/algorithm/topological_sorting.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <graaflib/graph.h>

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::vector<vertex_id_t> topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph);

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

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

#include <vector>
#include <unordered_map>

namespace graaf::algorithm {

namespace detail {

enum class vertex_status { UNVISITED, VISITED};

// 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_map<vertex_id_t, vertex_status>& processed_vertices,
std::vector<vertex_id_t>& sorted_vertices) {

processed_vertices[start_vertex] = vertex_status::VISITED;
for (const auto& next_vertex : graph.get_neighbors(start_vertex)) {
if (processed_vertices[next_vertex] == vertex_status::UNVISITED) {
do_dfs_topological_sort(graph, next_vertex, processed_vertices,
sorted_vertices);
}
}

sorted_vertices.push_back(start_vertex);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add the start_vertex to the sorted_vertices before the for-loop, do you already have the vertices recorded in the correct order? Maybe it would be possible to get rid of the std::reverse then.

Copy link
Contributor Author

@Hromz Hromz Sep 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about that. In this case, vertices will be stored as usual in DFS traversal, ignoring all other dependencies.
Consider this directed graph
0
/ \
1 2
\ /
3
If we do preorder, the vertices will be 0–1–3–2, which is incorrect for TS.
But if we do post order 0-1-2-3, Or maybe I missed something, correct me if I'm getting something wrong.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quickly checked @bobluppes's proposal and it indeed fails the tests. I would suggest going with the solution in this PR and think about ways getting rid of the std::reverse in a follow up.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad indeed. One thing we could investigate is to use a std::deque rather than a vector and call push_front. However, let's not optimize prematurely and let's tackle this in a follow up.

}

}; // namespace detail

template <typename V, typename E>
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 {};
}

using enum detail::vertex_status;
std::vector<vertex_id_t> sorted_vertices{};
sorted_vertices.reserve(graph.vertex_count());
std::unordered_map<vertex_id_t, detail::vertex_status> processed_vertices{};

for (const auto& vertex : graph.get_vertices()) {
if (processed_vertices[vertex.first] == UNVISITED) {
detail::do_dfs_topological_sort(graph, vertex.first, processed_vertices, sorted_vertices);
}
}

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

}; // namespace graaf::algorithm
192 changes: 192 additions & 0 deletions test/graaflib/algorithm/topological_sorting_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#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{0, 1, 2, 3};

// THEN
ASSERT_EQ(expected_vertices, 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);
std::vector<vertex_id_t> expected_vertices{};

// 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);
std::vector<vertex_id_t> expected_vertices{};

// 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);
std::vector<vertex_id_t> expected_vertices{};

// 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{0, 3, 1, 5, 4, 2, 6};
std::vector<vertex_id_t> expected_vertices_2{0, 1, 3, 5, 4, 2, 6};
std::vector<vertex_id_t> expected_vertices_3{0, 1, 3, 4, 5, 2, 6};
std::vector<vertex_id_t> expected_vertices_4{0, 4, 3, 1, 5, 2, 6};
std::vector<vertex_id_t> expected_vertices_5{0, 4, 1, 3, 5, 2, 6};
std::vector<vertex_id_t> expected_vertices_6{0, 1, 4, 3, 5, 2, 6};
std::vector<vertex_id_t> expected_vertices_7{0, 3, 1, 4, 5, 2, 6};
std::vector<vertex_id_t> expected_vertices_8{0, 3, 4, 1, 5, 2, 6};

// 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{0, 1, 2, 3, 4, 5, 6};
std::vector<vertex_id_t> expected_vertices_2{0, 1, 2, 5, 6, 3, 4};
std::vector<vertex_id_t> expected_vertices_3{0, 5, 6, 1, 2, 3, 4};
std::vector<vertex_id_t> expected_vertices_4{0, 5, 6, 3, 4, 1, 2};
std::vector<vertex_id_t> expected_vertices_5{0, 3, 4, 5, 6, 1, 2};
std::vector<vertex_id_t> expected_vertices_6{0, 3, 4, 1, 2, 5, 6};

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