From a4dc257790fb910ac143ad6c1df6899cac6d7e81 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Fri, 10 Apr 2026 15:28:31 -0400 Subject: [PATCH 1/2] Add Tarjan's SCC algorithm with iterative DFS - tarjan_scc.hpp: single-pass O(V+E) using low-link values, no transpose needed - 17 test cases: empty/single/cycle/DAG/complex/self-loops/weighted/disconnected - Includes agreement test with existing Kosaraju implementation --- include/graph/algorithm/tarjan_scc.hpp | 239 +++++++++++++++++ tests/algorithms/CMakeLists.txt | 1 + tests/algorithms/test_tarjan_scc.cpp | 339 +++++++++++++++++++++++++ 3 files changed, 579 insertions(+) create mode 100644 include/graph/algorithm/tarjan_scc.hpp create mode 100644 tests/algorithms/test_tarjan_scc.cpp diff --git a/include/graph/algorithm/tarjan_scc.hpp b/include/graph/algorithm/tarjan_scc.hpp new file mode 100644 index 0000000..c54477e --- /dev/null +++ b/include/graph/algorithm/tarjan_scc.hpp @@ -0,0 +1,239 @@ +/** + * @file tarjan_scc.hpp + * + * @brief Tarjan's Strongly Connected Components algorithm for directed graphs. + * + * Finds all strongly connected components (SCCs) in a directed graph using + * Tarjan's single-pass DFS algorithm with low-link values. + * + * @copyright Copyright (c) 2024 + * + * SPDX-License-Identifier: BSL-1.0 + * + * @authors + * Phil Ratzloff + */ + +#include "graph/graph.hpp" +#include "graph/views/vertexlist.hpp" +#include "graph/adj_list/vertex_property_map.hpp" +#include "graph/algorithm/traversal_common.hpp" + +#ifndef GRAPH_TARJAN_SCC_HPP +# define GRAPH_TARJAN_SCC_HPP + +# include +# include + +namespace graph { + +// Using declarations for new namespace structure +using adj_list::adjacency_list; +using adj_list::vertex_id_t; +using adj_list::vertices; +using adj_list::edges; +using adj_list::target_id; +using adj_list::vertex_id; +using adj_list::num_vertices; +using adj_list::find_vertex; + +/** + * @ingroup graph_algorithms + * @brief Find strongly connected components using Tarjan's algorithm. + * + * A strongly connected component (SCC) is a maximal set of vertices where every + * vertex is reachable from every other vertex via directed paths. Tarjan's + * algorithm discovers all SCCs in a single depth-first search using discovery + * times and low-link values. + * + * @tparam G The graph type. Must satisfy adjacency_list concept. + * @tparam ComponentFn Callable providing per-vertex component ID access: + * (const G&, vertex_id_t) -> ComponentID&. Must satisfy + * vertex_property_fn_for. + * + * @param g The directed graph to analyze + * @param component Callable providing per-vertex component access: component(g, uid) -> ComponentID&. + * For containers: wrap with container_value_fn(component). + * + * @return Number of strongly connected components found + * + * **Mandates:** + * - G must satisfy adjacency_list + * - ComponentFn must satisfy vertex_property_fn_for + * + * **Preconditions:** + * - component must contain an entry for each vertex of g + * + * **Effects:** + * - Sets component(g, uid) for all vertices via the component function + * - Does not modify the graph g + * + * **Postconditions:** + * - component(g, uid) contains the SCC ID for vertex uid + * - Component IDs are assigned 0, 1, 2, ... , num_components-1 + * - Vertices in the same SCC have the same component ID + * - Return value equals the number of distinct component IDs + * + * **Throws:** + * - std::bad_alloc if internal allocations fail + * - Exception guarantee: Basic. + * + * **Complexity:** + * - Time: O(V + E) — single DFS traversal visiting each vertex and edge once + * - Space: O(V) for discovery time, low-link, on-stack flag, DFS stack, and SCC stack + * + * **Remarks:** + * - Uses iterative DFS with explicit stack to avoid recursion-depth limits + * - Single-pass: requires only one DFS (vs Kosaraju's two passes) + * - Does not require a transpose graph + * - Low-link values track the earliest reachable ancestor in each subtree + * - An SCC root is identified when disc[u] == low[u] after processing all edges + * - Component IDs are assigned in reverse topological order of the SCC DAG + * + * **Supported Graph Properties:** + * + * Directedness: + * - ✅ Directed graphs (required) + * - ❌ Undirected graphs (use connected_components instead) + * + * Edge Properties: + * - ✅ Weighted edges (weights ignored) + * - ✅ Self-loops (handled correctly) + * - ✅ Multi-edges (treated as single edge) + * - ✅ Cycles + * + * Graph Structure: + * - ✅ Connected graphs + * - ✅ Disconnected graphs + * - ✅ Empty graphs (returns 0) + * + * ## Example Usage + * + * ```cpp + * #include + * #include + * + * using namespace graph; + * + * // Create directed graph: 0->1->2->0 (cycle), 2->3 + * Graph g({{0,1}, {1,2}, {2,0}, {2,3}}); + * + * std::vector component(num_vertices(g)); + * size_t num = tarjan_scc(g, container_value_fn(component)); + * // num = 2, component: vertices {0,1,2} share one ID, vertex 3 has another + * ``` + * + * @see kosaraju For two-pass SCC using transpose graph + * @see connected_components For undirected graphs + */ +template +requires vertex_property_fn_for +size_t tarjan_scc(G&& g, // graph + ComponentFn&& component // out: strongly connected component assignment +) { + using vid_t = vertex_id_t; + using CT = vertex_fn_value_t; + + const size_t N = num_vertices(g); + if (N == 0) { + return 0; + } + + constexpr size_t UNVISITED = std::numeric_limits::max(); + + auto disc = make_vertex_property_map, size_t>(g, UNVISITED); + auto low = make_vertex_property_map, size_t>(g, UNVISITED); + auto on_stack = make_vertex_property_map, bool>(g, false); + + // Initialize all components as unvisited + for (auto&& [uid, u] : views::vertexlist(g)) { + component(g, uid) = std::numeric_limits::max(); + } + + size_t timer = 0; + size_t cid = 0; + + // Tarjan's stack: vertices in the current DFS path and pending SCC assignment + std::stack scc_stack; + + // Iterative DFS: store edge iterators per frame to avoid re-scanning adjacency lists + using edge_iter_t = std::ranges::iterator_t()))>; + + struct dfs_frame { + vid_t uid; + edge_iter_t it; + edge_iter_t it_end; + }; + + std::stack dfs; + + // Outer loop: handle disconnected graphs + for (auto [start] : views::basic_vertexlist(g)) { + if (disc[start] != UNVISITED) { + continue; + } + + disc[start] = low[start] = timer++; + on_stack[start] = true; + scc_stack.push(start); + + auto start_edges = edges(g, start); + dfs.push({start, std::ranges::begin(start_edges), std::ranges::end(start_edges)}); + + while (!dfs.empty()) { + auto& [uid, it, it_end] = dfs.top(); + + if (it == it_end) { + // All edges processed — check if uid is an SCC root + if (disc[uid] == low[uid]) { + // Pop all vertices in this SCC from the Tarjan stack + vid_t w; + do { + w = scc_stack.top(); + scc_stack.pop(); + on_stack[w] = false; + component(g, w) = static_cast(cid); + } while (w != uid); + ++cid; + } + + // Backtrack: update parent's low-link + dfs.pop(); + if (!dfs.empty()) { + auto& [par_uid, par_it, par_it_end] = dfs.top(); + if (low[uid] < low[par_uid]) { + low[par_uid] = low[uid]; + } + } + continue; + } + + vid_t vid = target_id(g, *it); + ++it; // advance stored iterator for next resume + + if (disc[vid] == UNVISITED) { + // Tree edge: push new DFS frame + disc[vid] = low[vid] = timer++; + on_stack[vid] = true; + scc_stack.push(vid); + + auto vid_edges = edges(g, vid); + dfs.push({vid, std::ranges::begin(vid_edges), std::ranges::end(vid_edges)}); + } else if (on_stack[vid]) { + // Back/cross edge to vertex still on SCC stack: update low-link + if (disc[vid] < low[uid]) { + low[uid] = disc[vid]; + } + } + // If vid is already assigned to a completed SCC (on_stack[vid] == false), + // it's a cross edge to a finished SCC — do not update low-link. + } + } + + return cid; +} + +} // namespace graph + +#endif // GRAPH_TARJAN_SCC_HPP diff --git a/tests/algorithms/CMakeLists.txt b/tests/algorithms/CMakeLists.txt index 557cf55..657e2ed 100644 --- a/tests/algorithms/CMakeLists.txt +++ b/tests/algorithms/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(test_algorithms test_biconnected_components.cpp test_jaccard.cpp test_scc_bidirectional.cpp + test_tarjan_scc.cpp ) target_link_libraries(test_algorithms diff --git a/tests/algorithms/test_tarjan_scc.cpp b/tests/algorithms/test_tarjan_scc.cpp new file mode 100644 index 0000000..f63479e --- /dev/null +++ b/tests/algorithms/test_tarjan_scc.cpp @@ -0,0 +1,339 @@ +/** + * @file test_tarjan_scc.cpp + * @brief Tests for tarjan_scc() strongly connected components algorithm. + * + * Verifies: + * - Correctness of SCC detection using Tarjan's single-pass DFS + * - Agreement with Kosaraju's algorithm (two-graph overload) + * - Works with both vov (random-access) and vol (forward-iterator) containers + * - Edge cases: empty graph, single vertex, self-loops, disconnected graphs + */ + +#include +#include +#include +#include +#include +#include +#include "../common/graph_fixtures.hpp" +#include +#include +#include + +using namespace graph; +using namespace graph::container; +using namespace graph::test::fixtures; + +// Graph types for testing +using vov_void = dynamic_graph>; + +using vol_void = dynamic_graph>; + +using vov_int = dynamic_graph>; + +// ============================================================================= +// Helpers +// ============================================================================= + +template +bool all_same_component(const Component& component, const std::vector& vertices) { + if (vertices.empty()) + return true; + auto first_comp = component[vertices[0]]; + return std::all_of(vertices.begin(), vertices.end(), [&](size_t v) { return component[v] == first_comp; }); +} + +template +bool different_components(const Component& component, size_t u, size_t v) { + return component[u] != component[v]; +} + +template +size_t count_unique_components(const Component& component) { + std::set unique(component.begin(), component.end()); + return unique.size(); +} + +// ============================================================================= +// Empty graph +// ============================================================================= + +TEST_CASE("tarjan_scc - empty graph", "[algorithm][tarjan][scc]") { + vov_void g; + + std::vector component; + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 0); +} + +// ============================================================================= +// Single vertex +// ============================================================================= + +TEST_CASE("tarjan_scc - single vertex (vov)", "[algorithm][tarjan][scc]") { + auto g = single_vertex(); + + std::vector component(num_vertices(g)); + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 1); + REQUIRE(component[0] == 0); +} + +TEST_CASE("tarjan_scc - single vertex (vol)", "[algorithm][tarjan][scc]") { + auto g = single_vertex(); + + std::vector component(num_vertices(g)); + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 1); + REQUIRE(component[0] == 0); +} + +// ============================================================================= +// Simple cycle — all vertices in one SCC +// ============================================================================= + +TEST_CASE("tarjan_scc - simple cycle (vov)", "[algorithm][tarjan][scc]") { + // 0 -> 1 -> 2 -> 0 + vov_void g({{0, 1}, {1, 2}, {2, 0}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 1); + REQUIRE(all_same_component(component, {0, 1, 2})); +} + +TEST_CASE("tarjan_scc - simple cycle (vol)", "[algorithm][tarjan][scc]") { + vol_void g({{0, 1}, {1, 2}, {2, 0}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 1); + REQUIRE(all_same_component(component, {0, 1, 2})); +} + +// ============================================================================= +// Two SCCs +// ============================================================================= + +TEST_CASE("tarjan_scc - two SCCs (vov)", "[algorithm][tarjan][scc]") { + // SCC1: {0,1} (0 <-> 1) + // SCC2: {2,3} (2 <-> 3) + // Cross: 1 -> 2 (one-way, so no merge) + vov_void g({{0, 1}, {1, 0}, {1, 2}, {2, 3}, {3, 2}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 2); + REQUIRE(all_same_component(component, {0, 1})); + REQUIRE(all_same_component(component, {2, 3})); + REQUIRE(different_components(component, 0, 2)); +} + +TEST_CASE("tarjan_scc - two SCCs (vol)", "[algorithm][tarjan][scc]") { + vol_void g({{0, 1}, {1, 0}, {1, 2}, {2, 3}, {3, 2}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 2); + REQUIRE(all_same_component(component, {0, 1})); + REQUIRE(all_same_component(component, {2, 3})); + REQUIRE(different_components(component, 0, 2)); +} + +// ============================================================================= +// DAG — every vertex is its own SCC +// ============================================================================= + +TEST_CASE("tarjan_scc - DAG (vov)", "[algorithm][tarjan][scc]") { + // 0 -> 1 -> 2 -> 3 + vov_void g({{0, 1}, {1, 2}, {2, 3}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 4); + REQUIRE(count_unique_components(component) == 4); + for (size_t i = 0; i < 4; ++i) + for (size_t j = i + 1; j < 4; ++j) + REQUIRE(different_components(component, i, j)); +} + +TEST_CASE("tarjan_scc - DAG (vol)", "[algorithm][tarjan][scc]") { + vol_void g({{0, 1}, {1, 2}, {2, 3}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 4); + REQUIRE(count_unique_components(component) == 4); + for (size_t i = 0; i < 4; ++i) + for (size_t j = i + 1; j < 4; ++j) + REQUIRE(different_components(component, i, j)); +} + +// ============================================================================= +// Complex SCC structure (3 SCCs) +// ============================================================================= + +TEST_CASE("tarjan_scc - complex SCCs (vov)", "[algorithm][tarjan][scc]") { + // SCC1: {0,1,2} cycle 0->1->2->0 + // SCC2: {3,4} cycle 3->4->3 + // SCC3: {5} singleton + // Cross: 2->3, 4->5 + vov_void g({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + + std::vector component(num_vertices(g)); + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 3); + REQUIRE(count_unique_components(component) == 3); + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(all_same_component(component, {3, 4})); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 0, 5)); + REQUIRE(different_components(component, 3, 5)); +} + +TEST_CASE("tarjan_scc - complex SCCs (vol)", "[algorithm][tarjan][scc]") { + vol_void g({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + + std::vector component(num_vertices(g)); + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 3); + REQUIRE(count_unique_components(component) == 3); + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(all_same_component(component, {3, 4})); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 0, 5)); + REQUIRE(different_components(component, 3, 5)); +} + +// ============================================================================= +// Self-loops +// ============================================================================= + +TEST_CASE("tarjan_scc - self loops", "[algorithm][tarjan][scc]") { + // 0 self-loop, 1 self-loop, 0->1 (one-way) + vov_void g({{0, 0}, {1, 1}, {0, 1}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + // Each vertex is its own SCC (self-loop doesn't merge with others) + REQUIRE(num == 2); + REQUIRE(count_unique_components(component) == 2); + REQUIRE(different_components(component, 0, 1)); +} + +// ============================================================================= +// Weighted edges (weights ignored) +// ============================================================================= + +TEST_CASE("tarjan_scc - weighted edges ignored", "[algorithm][tarjan][scc]") { + // Same structure as simple cycle but with weights + vov_int g({{0, 1, 10}, {1, 2, 20}, {2, 0, 30}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 1); + REQUIRE(all_same_component(component, {0, 1, 2})); +} + +// ============================================================================= +// Disconnected graph +// ============================================================================= + +TEST_CASE("tarjan_scc - disconnected graph", "[algorithm][tarjan][scc]") { + // 0->1->0 (SCC), 2 isolated, 3->4->3 (SCC) + vov_void g({{0, 1}, {1, 0}, {3, 4}, {4, 3}}); + + std::vector component(num_vertices(g)); + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 3); + REQUIRE(count_unique_components(component) == 3); + REQUIRE(all_same_component(component, {0, 1})); + REQUIRE(all_same_component(component, {3, 4})); + REQUIRE(different_components(component, 0, 2)); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 2, 3)); +} + +// ============================================================================= +// Agreement with Kosaraju (two-graph overload) +// ============================================================================= + +TEST_CASE("tarjan_scc - agrees with kosaraju", "[algorithm][tarjan][scc]") { + // Graph: 0->1->2->0 (SCC), 2->3->4->3 (SCC), 4->5 + vov_void g({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 3}, {4, 5}}); + vov_void g_t({{1, 0}, {2, 1}, {0, 2}, {3, 2}, {4, 3}, {3, 4}, {5, 4}}); + + std::vector comp_tarjan(num_vertices(g)); + std::vector comp_kosaraju(num_vertices(g)); + + size_t num_tarjan = tarjan_scc(g, container_value_fn(comp_tarjan)); + kosaraju(g, g_t, container_value_fn(comp_kosaraju)); + + // Both should find the same number of SCCs + REQUIRE(num_tarjan == count_unique_components(comp_kosaraju)); + + // Both should agree on which vertices are in the same SCC + for (size_t i = 0; i < comp_tarjan.size(); ++i) + for (size_t j = i + 1; j < comp_tarjan.size(); ++j) { + INFO("vertices " << i << " and " << j); + REQUIRE((comp_tarjan[i] == comp_tarjan[j]) == (comp_kosaraju[i] == comp_kosaraju[j])); + } +} + +// ============================================================================= +// Larger graph: Wikipedia Tarjan example +// ============================================================================= + +TEST_CASE("tarjan_scc - wikipedia example", "[algorithm][tarjan][scc]") { + // Classic 8-vertex example: + // SCC1: {0,1,2} (0->1, 1->2, 2->0) + // SCC2: {3,4,5} (3->4, 4->5, 5->3) + // SCC3: {6,7} (6->7, 7->6) + // Cross: 2->3, 5->6, 4->0 creates link back but 4->0 merges into SCC? No. + // Let's use the standard example: + // 0->1, 1->2, 2->0, 2->3, 3->4, 4->5, 5->3, 5->6, 6->7, 7->6 + vov_void g({{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 5}, {5, 3}, {5, 6}, {6, 7}, {7, 6}}); + + std::vector component(num_vertices(g)); + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 3); + REQUIRE(all_same_component(component, {0, 1, 2})); + REQUIRE(all_same_component(component, {3, 4, 5})); + REQUIRE(all_same_component(component, {6, 7})); + REQUIRE(different_components(component, 0, 3)); + REQUIRE(different_components(component, 0, 6)); + REQUIRE(different_components(component, 3, 6)); +} + +// ============================================================================= +// Single large SCC (complete cycle) +// ============================================================================= + +TEST_CASE("tarjan_scc - complete cycle", "[algorithm][tarjan][scc]") { + // 0 -> 1 -> 2 -> 3 -> 4 -> 0 + vov_void g({{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}}); + std::vector component(num_vertices(g)); + + size_t num = tarjan_scc(g, container_value_fn(component)); + + REQUIRE(num == 1); + REQUIRE(all_same_component(component, {0, 1, 2, 3, 4})); +} From ccda40278d227e8ff24b58fcf99eba73ab2c5b57 Mon Sep 17 00:00:00 2001 From: Phil Ratzloff Date: Fri, 10 Apr 2026 15:45:03 -0400 Subject: [PATCH 2/2] Add Tarjan SCC documentation - README.md: algorithm count 13->14, added Tarjan SCC to feature lists - implementation_matrix.md: count 13->14, added tarjan_scc.hpp row - algorithms.md: added Tarjan to Components category and alphabetical tables - algorithm-complexity.md: added Tarjan row and header file listing - connected_components.md: added Tarjan to overview, When to Use, See Also - tarjan_scc.md: new dedicated algorithm doc page - CHANGELOG.md: added Tarjan SCC entry to Unreleased section --- CHANGELOG.md | 2 + README.md | 4 +- docs/reference/algorithm-complexity.md | 2 + docs/status/implementation_matrix.md | 3 +- docs/user-guide/algorithms.md | 2 + .../algorithms/connected_components.md | 5 + docs/user-guide/algorithms/tarjan_scc.md | 169 ++++++++++++++++++ 7 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 docs/user-guide/algorithms/tarjan_scc.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 820e44d..774cce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Added +- **Tarjan's SCC algorithm** (`tarjan_scc.hpp`) — single-pass O(V+E) strongly connected components using iterative DFS with low-link values; no transpose graph needed +- 17 new Tarjan SCC tests (`test_tarjan_scc.cpp`) - **Mapped (sparse) graph algorithm support** — all 14 algorithms now accept `adjacency_list` (both index and map-based containers) - `mapped_vertex_range`, `mapped_adjacency_list`, `mapped_bidirectional_adjacency_list` concepts - `vertex_property_map` type alias and `make_vertex_property_map` factory (vector for index graphs, unordered_map for mapped) diff --git a/README.md b/README.md index a18e0fc..2ee34a5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ - **Header-only** — drop into any CMake project; no compiled components - **Generic** - Enabled by the use of descriptors. - **Works with your graphs** — Bring your own graph. `std::vector>` and `std::map>>` are also valid graphs out of the box. -- **13 algorithms** — Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard coefficient +- **14 algorithms** — Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, Tarjan SCC, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard coefficient - **7 lazy views** — vertexlist, edgelist, incidence, neighbors, BFS, DFS, topological sort — all composable with range adaptors - **Bidirectional edge access** — `in_edges`, `in_degree`, reverse BFS/DFS/topological sort via `in_edge_accessor` - **Customization Point Objects (CPOs)** — adapt existing data structures without modifying them @@ -96,7 +96,7 @@ Both share a common descriptor system and customization-point interface. | Category | What's Included | Details | |----------|-----------------|---------| -| **Algorithms** | Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard | [Algorithm reference](docs/status/implementation_matrix.md#algorithms) | +| **Algorithms** | Dijkstra, Bellman-Ford, BFS, DFS, topological sort, connected components, Tarjan SCC, articulation points, biconnected components, MST, triangle counting, MIS, label propagation, Jaccard | [Algorithm reference](docs/status/implementation_matrix.md#algorithms) | | **Views** | vertexlist, edgelist, incidence, neighbors, BFS, DFS, topological sort | [View reference](docs/status/implementation_matrix.md#views) | | **Containers** | `dynamic_graph` (27 trait combos), `compressed_graph` (CSR), `undirected_adjacency_list` | [Container reference](docs/status/implementation_matrix.md#containers) | | **CPOs** | 19 customization point objects (vertices, edges, target_id, vertex_value, edge_value, …) | [CPO reference](docs/reference/cpo-reference.md) | diff --git a/docs/reference/algorithm-complexity.md b/docs/reference/algorithm-complexity.md index 8ce4100..c62cd44 100644 --- a/docs/reference/algorithm-complexity.md +++ b/docs/reference/algorithm-complexity.md @@ -47,6 +47,7 @@ headers for every algorithm in graph-v3. |-----------|----------|------|-------|-------------------|--------| | **Connected Components** | `connected_components` | O(V+E) | O(V) | `index_adjacency_list` | `connected_components.hpp` | | **Kosaraju (SCC)** | `kosaraju` | O(V+E) | O(V) | `index_adjacency_list` (graph + transpose) | `connected_components.hpp` | +| **Tarjan (SCC)** | `tarjan_scc` | O(V+E) | O(V) | `adjacency_list` | `tarjan_scc.hpp` | | **Afforest** | `afforest` | O(V + E·α(V)) | O(V) | `index_adjacency_list` | `connected_components.hpp` | | **Biconnected Components** | `biconnected_components` | O(V+E) | O(V+E) | `index_adjacency_list` | `biconnected_components.hpp` | @@ -96,6 +97,7 @@ algorithm/ ├── jaccard.hpp ├── label_propagation.hpp ├── mst.hpp +├── tarjan_scc.hpp ├── topological_sort.hpp └── traversal_common.hpp (shared types: visitors, init helpers) ``` diff --git a/docs/status/implementation_matrix.md b/docs/status/implementation_matrix.md index aee159e..3774a9e 100644 --- a/docs/status/implementation_matrix.md +++ b/docs/status/implementation_matrix.md @@ -15,7 +15,7 @@ ## Algorithms -13 implemented algorithms in `include/graph/algorithm/` (excluding `traversal_common.hpp`): +14 implemented algorithms in `include/graph/algorithm/` (excluding `traversal_common.hpp`): | Algorithm | Header | Test File | Status | |-----------|--------|-----------|--------| @@ -25,6 +25,7 @@ | Depth-first search | `depth_first_search.hpp` | `test_depth_first_search.cpp` | Implemented | | Topological sort | `topological_sort.hpp` | `test_topological_sort.cpp` | Implemented | | Connected components (Kosaraju SCC) | `connected_components.hpp` | `test_connected_components.cpp`, `test_scc_bidirectional.cpp` | Implemented | +| Tarjan SCC | `tarjan_scc.hpp` | `test_tarjan_scc.cpp` | Implemented | | Articulation points | `articulation_points.hpp` | `test_articulation_points.cpp` | Implemented | | Biconnected components | `biconnected_components.hpp` | `test_biconnected_components.cpp` | Implemented | | MST (Prim / Kruskal) | `mst.hpp` | `test_mst.cpp` | Implemented | diff --git a/docs/user-guide/algorithms.md b/docs/user-guide/algorithms.md index 35e19e4..8048852 100644 --- a/docs/user-guide/algorithms.md +++ b/docs/user-guide/algorithms.md @@ -110,6 +110,7 @@ All headers are under `include/graph/algorithm/`. | [Articulation Points](algorithms/articulation_points.md) | `articulation_points.hpp` | Cut vertices whose removal disconnects the graph | O(V+E) | O(V) | | [Biconnected Components](algorithms/biconnected_components.md) | `biconnected_components.hpp` | Maximal 2-connected subgraphs (Hopcroft-Tarjan) | O(V+E) | O(V+E) | | [Connected Components](algorithms/connected_components.md) | `connected_components.hpp` | Undirected CC, directed SCC (Kosaraju), union-find (afforest) | O(V+E) | O(V) | +| [Tarjan SCC](algorithms/tarjan_scc.md) | `tarjan_scc.hpp` | Single-pass directed SCC via low-link values | O(V+E) | O(V) | **Minimum Spanning Trees** @@ -145,6 +146,7 @@ All headers are under `include/graph/algorithm/`. | [Maximal Independent Set](algorithms/mis.md) | Analytics | `mis.hpp` | O(V+E) | O(V) | | [Prim MST](algorithms/mst.md#prims-algorithm) | MST | `mst.hpp` | O(E log V) | O(V) | | [Topological Sort](algorithms/topological_sort.md) | Traversal | `topological_sort.hpp` | O(V+E) | O(V) | +| [Tarjan SCC](algorithms/tarjan_scc.md) | Components | `tarjan_scc.hpp` | O(V+E) | O(V) | | [Triangle Count](algorithms/triangle_count.md) | Analytics | `tc.hpp` | O(m^{3/2}) | O(1) | --- diff --git a/docs/user-guide/algorithms/connected_components.md b/docs/user-guide/algorithms/connected_components.md index 4120427..13bdf9c 100644 --- a/docs/user-guide/algorithms/connected_components.md +++ b/docs/user-guide/algorithms/connected_components.md @@ -45,6 +45,7 @@ map-based (sparse vertex ID) graphs are supported. |-----------|----------|----------| | `connected_components` | Undirected graphs | DFS-based | | `kosaraju` | Directed graphs (SCC) | Two DFS passes (requires transpose) | +| `tarjan_scc` | Directed graphs (SCC) | Single DFS pass (no transpose needed) | | `afforest` | Large graphs, parallel-friendly | Union-find with neighbor sampling | All three fill a `component` array where `component[v]` is the component ID for @@ -56,6 +57,9 @@ vertex v. the number of components directly. - **`kosaraju`** — when you need strongly connected components of a directed graph. Requires constructing the transpose graph (all edges reversed). +- **`tarjan_scc`** — when you need SCCs without constructing a transpose graph. + Single-pass DFS using low-link values. Returns the number of SCCs. + See [Tarjan SCC](tarjan_scc.md). - **`afforest`** — when working with large graphs or when you intend to parallelize later. Uses union-find with neighbor sampling, which has good cache behavior on large inputs. @@ -313,6 +317,7 @@ compress(comp); ## See Also +- [Tarjan SCC](tarjan_scc.md) — single-pass SCC algorithm (no transpose needed) - [Biconnected Components](biconnected_components.md) — maximal 2-connected subgraphs - [Articulation Points](articulation_points.md) — cut vertices - [Algorithm Catalog](../algorithms.md) — full list of algorithms diff --git a/docs/user-guide/algorithms/tarjan_scc.md b/docs/user-guide/algorithms/tarjan_scc.md new file mode 100644 index 0000000..616d2e2 --- /dev/null +++ b/docs/user-guide/algorithms/tarjan_scc.md @@ -0,0 +1,169 @@ + + + +
graph-v3 logo + +# Tarjan's Strongly Connected Components + +
+ +> [← Back to Algorithm Catalog](../algorithms.md) + +## Table of Contents +- [Overview](#overview) +- [When to Use](#when-to-use) +- [Include](#include) +- [Algorithm](#algorithm) +- [Parameters](#parameters) +- [Supported Graph Properties](#supported-graph-properties) +- [Examples](#examples) +- [Mandates](#mandates) +- [Preconditions](#preconditions) +- [Effects](#effects) +- [Returns](#returns) +- [Throws](#throws) +- [Complexity](#complexity) +- [See Also](#see-also) + +## Overview + +Tarjan's algorithm finds all strongly connected components (SCCs) in a directed +graph using a single depth-first search. It uses discovery times and low-link +values to identify SCC roots, then pops completed SCCs from an auxiliary stack. + +Unlike Kosaraju's algorithm, Tarjan's requires **no transpose graph** and performs +only **one DFS pass**, making it simpler to use when a transpose is unavailable. + +| Property | Value | +|----------|-------| +| Passes | 1 (single DFS) | +| Transpose needed | No | +| SCC order | Reverse topological | +| Return value | Number of SCCs | + +## When to Use + +- **Use `tarjan_scc`** when you need SCCs and don't have (or don't want to + construct) a transpose graph. Single-pass, no extra graph required. +- **Use `kosaraju`** when you already have a transpose or bidirectional graph, + or need topological SCC ordering. +- **Use `connected_components`** for undirected graphs. + +## Include + +```cpp +#include +``` + +## Algorithm + +```cpp +size_t tarjan_scc(G&& g, ComponentFn&& component); +``` + +Single-pass iterative DFS using low-link values. Fills `component(g, uid)` with +the SCC ID for each vertex and returns the total number of SCCs. + +## Parameters + +| Parameter | Description | +|-----------|-------------| +| `g` | Graph satisfying `adjacency_list` | +| `component` | Callable `(const G&, vertex_id_t) -> ComponentID&` returning a mutable reference. For containers: wrap with `container_value_fn(comp)`. Must satisfy `vertex_property_fn_for`. | + +**Return value:** `size_t` — number of strongly connected components. + +## Supported Graph Properties + +**Directedness:** +- ✅ Directed graphs (required) +- ❌ Undirected graphs (use `connected_components` instead) + +**Edge Properties:** +- ✅ Weighted edges (weights ignored) +- ✅ Self-loops (handled correctly) +- ✅ Multi-edges (treated as single edge) +- ✅ Cycles + +**Graph Structure:** +- ✅ Connected graphs +- ✅ Disconnected graphs (processes all components) +- ✅ Empty graphs (returns 0) + +**Container Requirements:** +- Required: `adjacency_list` +- `component` must satisfy `vertex_property_fn_for` + +## Examples + +### Example 1: Basic SCC Detection + +```cpp +#include +#include +#include + +using Graph = container::dynamic_graph>; + +// Directed graph: 0→1→2→0 (cycle = 1 SCC), 2→3 (singleton SCC) +Graph g({{0, 1}, {1, 2}, {2, 0}, {2, 3}}); + +std::vector comp(num_vertices(g)); +size_t num_scc = tarjan_scc(g, container_value_fn(comp)); + +// num_scc == 2 +// comp[0] == comp[1] == comp[2] (cycle forms one SCC) +// comp[3] != comp[0] (3 is a singleton SCC) +``` + +### Example 2: Comparing with Kosaraju + +```cpp +// Same graph, both algorithms +std::vector comp_tarjan(num_vertices(g)); +std::vector comp_kosaraju(num_vertices(g)); + +size_t n = tarjan_scc(g, container_value_fn(comp_tarjan)); +kosaraju(g, g_transpose, container_value_fn(comp_kosaraju)); + +// Both find the same SCCs (component IDs may differ, but groupings match) +``` + +## Mandates + +- `G` must satisfy `adjacency_list` +- `ComponentFn` must satisfy `vertex_property_fn_for` + +## Preconditions + +- `component(g, uid)` must be valid for all vertex IDs in `g` + +## Effects + +- Writes SCC IDs via `component(g, uid)` for all vertices +- Does not modify the graph `g` + +## Returns + +`size_t` — the number of strongly connected components. + +## Throws + +- `std::bad_alloc` if internal allocations fail +- Exception guarantee: Basic. Graph `g` remains unchanged; component output may be partial. + +## Complexity + +| Metric | Value | +|--------|-------| +| Time | O(V + E) | +| Space | O(V) | + +## See Also + +- [Connected Components](connected_components.md) — Kosaraju SCC, undirected CC, afforest +- [Articulation Points](articulation_points.md) — cut vertices (also uses Tarjan-style low-link) +- [Biconnected Components](biconnected_components.md) — maximal 2-connected subgraphs +- [Algorithm Catalog](../algorithms.md) — full list of algorithms +- [test_tarjan_scc.cpp](../../../tests/algorithms/test_tarjan_scc.cpp) — test suite