Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ take a look at the [docs](https://bobluppes.github.io/graaf/algorithms/intro.htm
5. [**Strongly Connected Components Algorithms**](https://bobluppes.github.io/graaf/algorithms/strongly-connected-components/):
- [Tarjan's Strongly Connected Components](https://bobluppes.github.io/graaf/algorithms/strongly-connected-components/tarjan.html)
- [Kosaraju's Strongly Connected Components](https://bobluppes.github.io/graaf/algorithms/strongly-connected-components/kosarajus.html)
6. [**Topological Sorting Algorithms**](https://bobluppes.github.io/graaf/algorithms/topological-sort/topological-sort.html):
6. [**Topological Sorting Algorithms**](https://bobluppes.github.io/graaf/algorithms/topological-sort/):
- [DFS Based Topological Sort](https://bobluppes.github.io/graaf/algorithms/topological-sort/topological-sort.html)
- [Kahn's Algorithm](https://bobluppes.github.io/graaf/algorithms/topological-sort/kahn.html)
7. [**Traversal Algorithms**](https://bobluppes.github.io/graaf/algorithms/traversal/):
- [Breadth-First Search (BFS)](https://bobluppes.github.io/graaf/algorithms/traversal/breadth-first-search.html)
- [Depth-First Search (DFS)](https://bobluppes.github.io/graaf/algorithms/traversal/depth-first-search.html)
Expand Down
4 changes: 3 additions & 1 deletion docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
- [Strongly Connected Component Algorithms](algorithms/strongly-connected-components/README.md)
- [Kosaraju's Strongly Connected Components](algorithms/strongly-connected-components/kosarajus.md)
- [Tarjan's Strongly Connected Components](algorithms/strongly-connected-components/tarjan.md)
- [Topological sort algorithm](algorithms/topological-sort/topological-sort.md)
- [Topological Sorting Algorithms](algorithms/topological-sort/README.md)
- [Topological sort algorithm](algorithms/topological-sort/topological-sort.md)
- [Kahn's Algorithm](algorithms/topological-sort/kahn.md)
- [Traversal Algorithms](algorithms/traversal/README.md)
- [Breadth First Search (BFS)](algorithms/traversal/breadth-first-search.md)
- [Depth First Search (DFS)](algorithms/traversal/depth-first-search.md)
Expand Down
1 change: 1 addition & 0 deletions docs/src/algorithms/topological-sort/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Topological Sorting Algorithms
70 changes: 70 additions & 0 deletions docs/src/algorithms/topological-sort/kahn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Kahn's Algorithm

Kahn's algorithm produces a topological ordering of a DAG (directed acyclic graph) by repeatedly emitting vertices that
have no remaining incoming edges.

The algorithm first counts the in-degree of every vertex. Every vertex with an in-degree of zero is a valid starting
point, so all of them are placed in a queue. Vertices are then taken off the queue one at a time and appended to the
result. Taking a vertex conceptually removes its outgoing edges, so the in-degree of each of its neighbors is decreased
by one, and any neighbor whose in-degree drops to zero is pushed onto the queue in turn.

Cycle detection falls out of the algorithm for free. A vertex on a cycle always has an incoming edge from another vertex
on that same cycle, so its in-degree never reaches zero and it is never emitted. If the result does not contain every
vertex of the graph, the graph contains a cycle and no topological ordering exists.

The runtime of the algorithm is `O(|V| + |E|)` and the memory consumption is `O(|V|)`. Where V is the number of vertices
in the graph and E the number of edges. Each vertex enters and leaves the queue exactly once, and each edge is relaxed
exactly once, when its source vertex is emitted.

[wikipedia](https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm)

## Syntax

```cpp
template <typename V, typename E>
[[nodiscard]] std::vector<vertex_id_t> kahn_topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph);
```

- **graph** The directed graph to traverse.
- **return** Vector of vertices sorted in topological order.
- **throws** `std::invalid_argument` if the graph contains a cycle, mirroring how `bellman_ford_shortest_paths` reports
a negative cycle.

A graph generally admits more than one valid topological ordering. Which one is returned depends on the order in which
vertices with an in-degree of zero are encountered, so callers should not depend on a particular ordering beyond it
being topologically valid.

## Comparison with the DFS based approach

Graaf also ships a [DFS based topological sort](topological-sort.md). Both produce a valid topological ordering in
`O(|V| + |E|)` time using `O(|V|)` additional memory, and for a graph with several valid orderings the two will
generally return different ones. They differ in a few practical respects.

| | `kahn_topological_sort` | `dfs_topological_sort` |
| --- | --- | --- |
| Traversal | BFS-like, explicit queue | DFS, recursive |
| Cycle detection | Falls out of the algorithm itself | Separate `dfs_cycle_detection` pass up front |
| Graph passes | One | Two (cycle detection, then the sort) |
| Reports a cycle by | Throwing `std::invalid_argument` | Returning `std::nullopt` |
| Return type | `std::vector<vertex_id_t>` | `std::optional<std::vector<vertex_id_t>>` |

Because Kahn's algorithm is iterative, its memory use does not depend on the depth of the graph. The DFS based version
recurses once per vertex along a path, so a graph with a very long path can exhaust the call stack; it inherits that
limitation from the DFS traversal it is built on. Prefer Kahn's algorithm for deep or very large graphs.

Kahn's algorithm also lends itself to *layered* processing. Every vertex sitting in the queue at the start of an
iteration has all of its dependencies satisfied, so a batch drained from the queue in one go forms a dependency
"level" whose vertices can be processed in parallel. This is a natural fit for build systems and job schedulers. The
DFS based version does not expose that structure.

Conversely, the DFS based version returns an `optional` rather than throwing, which fits better in code paths where a
cyclic graph is an expected outcome rather than an error.

## Use cases

- Build systems and package managers, ordering targets so dependencies are built first.
- Job and task schedulers, including parallel schedulers that use the level structure described above.
- Course or curriculum ordering with prerequisites.
- Spreadsheet formula evaluation order.
- Detecting whether a dependency graph has become circular.
5 changes: 5 additions & 0 deletions docs/src/algorithms/topological-sort/topological-sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ template <typename V, typename E>

- **graph** The directed graph to traverse.
- **return** Vector of vertices sorted in topological order. If the graph contains cycles, it returns std::nullopt.

## Similar algorithms

Graaf also implements [Kahn's algorithm](kahn.md), the BFS/in-degree based alternative. That page includes a
side-by-side comparison of the two approaches.
2 changes: 1 addition & 1 deletion docs/src/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Graaf requires no separate compilation: consume it as a header-only library, or
- [Graph coloring](https://bobluppes.github.io/graaf/algorithms/coloring/README.html): greedy, Welsh-Powell.
- [Strongly connected components](https://bobluppes.github.io/graaf/algorithms/strongly-connected-components/README.html): Kosaraju, Tarjan.
- [Clique detection](https://bobluppes.github.io/graaf/algorithms/clique-detection/bron_kerbosch.html): Bron-Kerbosch.
- [Topological sort](https://bobluppes.github.io/graaf/algorithms/topological-sort/topological-sort.html)
- [Topological sort](https://bobluppes.github.io/graaf/algorithms/topological-sort/README.html): DFS-based, Kahn.

## Examples

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include <graaflib/graph.h>

#include <vector>

namespace graaf::algorithm {
/**
* @brief Calculates order of vertices in topological order
* using Kahn's algorithm
*
* Repeatedly emits vertices with an in-degree of zero, removing their
* outgoing edges as it goes. Cycles are detected as a side effect: if not
* every vertex can be emitted, the remaining vertices lie on or downstream
* of a cycle.
*
* @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
* @throws std::invalid_argument If the graph contains a cycle.
*/
template <typename V, typename E>
[[nodiscard]] std::vector<vertex_id_t> kahn_topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph);

} // namespace graaf::algorithm
#include "kahn_topological_sorting.tpp"
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#pragma once
#include <graaflib/algorithm/topological_sorting/kahn_topological_sorting.h>

#include <cstddef>
#include <queue>
#include <stdexcept>
#include <unordered_map>
#include <vector>

#include "kahn_topological_sorting.h"

namespace graaf::algorithm {

namespace detail {

// Computes the in-degree of every vertex in a single pass over the adjacency
// lists. Note that properties::vertex_indegree scans the entire graph on every
// call, so seeding the in-degrees with it would cost O(|V| * (|V| + |E|)).
// Kahn's algorithm is expected to run in O(|V| + |E|), so we count the incoming
// edges in one sweep instead.
template <typename V, typename E>
[[nodiscard]] std::unordered_map<vertex_id_t, std::size_t> compute_indegrees(
const graph<V, E, graph_type::DIRECTED>& graph) {
std::unordered_map<vertex_id_t, std::size_t> indegrees{};
indegrees.reserve(graph.vertex_count());

for (const auto& [vertex_id, _] : graph.get_vertices()) {
indegrees.try_emplace(vertex_id, 0);
}

for (const auto& [vertex_id, _] : graph.get_vertices()) {
for (const auto& neighbor : graph.get_neighbors(vertex_id)) {
++indegrees[neighbor];
}
}

return indegrees;
}

}; // namespace detail

template <typename V, typename E>
std::vector<vertex_id_t> kahn_topological_sort(
const graph<V, E, graph_type::DIRECTED>& graph) {
auto indegrees{detail::compute_indegrees(graph)};

// All vertices without incoming edges can be emitted right away
std::queue<vertex_id_t> sources{};
for (const auto& [vertex_id, indegree] : indegrees) {
if (indegree == 0) {
sources.push(vertex_id);
}
}

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

while (!sources.empty()) {
const auto current_vertex{sources.front()};
sources.pop();
sorted_vertices.push_back(current_vertex);

// Removing the outgoing edges of the current vertex can turn its
// neighbors into sources
for (const auto& neighbor : graph.get_neighbors(current_vertex)) {
if (--indegrees[neighbor] == 0) {
sources.push(neighbor);
}
}
}

// Any vertex we could not emit still has an incoming edge, which means it
// lies on a cycle or is reachable from one. A self-loop is covered by this
// as well, since it contributes one to the in-degree of its own vertex.
if (sorted_vertices.size() != graph.vertex_count()) {
throw std::invalid_argument{"Cycle detected in the graph."};
}

return sorted_vertices;
}

}; // namespace graaf::algorithm
Loading
Loading