Tarjan.hpp
Go to the documentation of this file.
1/*
2 Copyright 2026 Equinor ASA.
3
4 This file is part of the Open Porous Media project (OPM).
5
6 OPM is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 OPM is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with OPM. If not, see <http://www.gnu.org/licenses/>.
18*/
19
20#ifndef OPM_TARJAN_HEADER_INCLUDED
21#define OPM_TARJAN_HEADER_INCLUDED
22
23#include <algorithm>
24#include <cstddef>
25#include <ranges>
26#include <span>
27#include <stdexcept>
28#include <string>
29#include <type_traits>
30#include <utility>
31#include <vector>
32
50
51namespace Opm
52{
53
62{
65 std::vector<int> vertices;
66
69 std::vector<int> offsets {0};
70
72 std::vector<int> componentOf;
73
77 int numComponents() const
78 {
79 return static_cast<int>(offsets.size()) - 1;
80 }
81
86 std::span<const int> operator[](int k) const
87 {
88 const auto first = static_cast<std::size_t>(offsets[k]);
89 const auto last = static_cast<std::size_t>(offsets[k + 1]);
90 return std::span<const int> {vertices}.subspan(first, last - first);
91 }
92
96 auto components() const
97 {
98 return std::views::iota(0, numComponents())
99 | std::views::transform([this](int k) { return (*this)[k]; });
100 }
101};
102
114template <class Graph>
115StronglyConnectedComponents
116tarjan(const Graph& graph)
117{
118 using Row = decltype(graph[std::size_t {}]);
119 using Iterator = std::ranges::iterator_t<Row>;
120 static_assert(std::ranges::forward_range<Row>, "A graph row must be a forward range");
121 static_assert(std::is_integral_v<std::ranges::range_value_t<Row>>,
122 "A graph row must hold integer vertex indices");
123
124 struct Frame
125 {
126 int vertex;
127 Iterator next;
128 Iterator end;
129 };
130
131 constexpr int unvisited = -1;
132 const int numVertices = static_cast<int>(std::ranges::size(graph));
133
135 result.vertices.reserve(numVertices);
136 result.componentOf.assign(numVertices, unvisited);
137
138 // order[v]: discovery number of v. low[v]: smallest discovery number reachable from v through
139 // its search subtree and at most one edge to a vertex that is still waiting to be assigned to a
140 // component.
141 std::vector<int> order(numVertices, unvisited);
142 std::vector<int> low(numVertices, 0);
143 std::vector<int> pending; // Visited but not yet assigned to a component.
144 std::vector<Frame> search; // Explicit stack replacing the recursion.
145 int discovered = 0;
146
147 const auto discover = [&](int vertex) {
148 order[vertex] = low[vertex] = discovered++;
149 pending.push_back(vertex);
150 auto&& row = graph[static_cast<std::size_t>(vertex)];
151 search.push_back({vertex, std::ranges::begin(row), std::ranges::end(row)});
152 };
153
154 for (int root = 0; root < numVertices; ++root) {
155 if (order[root] != unvisited) {
156 continue;
157 }
158 discover(root);
159
160 while (!search.empty()) {
161 // Do not keep a reference into 'search' across discover().
162 const int v = search.back().vertex;
163
164 if (search.back().next != search.back().end) {
165 const auto target = *search.back().next++;
166 if (std::cmp_less(target, 0) || std::cmp_greater_equal(target, numVertices)) {
167 throw std::out_of_range("Opm::tarjan: edge from vertex " + std::to_string(v)
168 + " to vertex " + std::to_string(target)
169 + " in a graph with " + std::to_string(numVertices)
170 + " vertices");
171 }
172 const int w = static_cast<int>(target);
173
174 if (order[w] == unvisited) {
175 discover(w);
176 } else if (result.componentOf[w] == unvisited) {
177 // w is on the 'pending' stack: v can reach back to it.
178 low[v] = std::min(low[v], order[w]);
179 }
180 continue;
181 }
182
183 // All edges of v are handled. If nothing in its subtree reaches a vertex discovered
184 // before v, v is the root of a component made up of everything above it on the
185 // 'pending' stack.
186 if (low[v] == order[v]) {
187 const int component = result.numComponents();
188 int member;
189 do {
190 member = pending.back();
191 pending.pop_back();
192 result.componentOf[member] = component;
193 result.vertices.push_back(member);
194 } while (member != v);
195 result.offsets.push_back(static_cast<int>(result.vertices.size()));
196 }
197
198 search.pop_back();
199 if (!search.empty()) {
200 const int parent = search.back().vertex;
201 low[parent] = std::min(low[parent], low[v]);
202 }
203 }
204 }
205
206 return result;
207}
208
209} // namespace Opm
210
211#endif // OPM_TARJAN_HEADER_INCLUDED
Definition: blackoilbioeffectsmodules.hh:45
StronglyConnectedComponents tarjan(const Graph &graph)
Compute the strongly connected components of a directed graph with Tarjan's algorithm.
Definition: Tarjan.hpp:116
std::string to_string(const ConvergenceReport::ReservoirFailure::Type t)
The strongly connected components of a directed graph, as computed by tarjan().
Definition: Tarjan.hpp:62
int numComponents() const
The number of components.
Definition: Tarjan.hpp:77
auto components() const
All components in order, for use in a range-based for loop.
Definition: Tarjan.hpp:96
std::vector< int > componentOf
The component each vertex belongs to.
Definition: Tarjan.hpp:72
std::span< const int > operator[](int k) const
The vertices of a component.
Definition: Tarjan.hpp:86
std::vector< int > vertices
Definition: Tarjan.hpp:65
std::vector< int > offsets
Definition: Tarjan.hpp:69