LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
stronglyconncomp.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_MC_STRONGLYCONNCOMP_H
6#define LINE_API_MC_STRONGLYCONNCOMP_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Strongly connected components of a directed graph, and which of them are
12 * recurrent (closed under the successor relation).
13 *
14 * Templated port of matlab/util/stronglyconncomp.m, the decomposition on which
15 * dtmc_solve_reducible, ctmc_solve_reducible and
16 * ctmc_solve_reducible_blkdecomp all rest. Tarjan's algorithm, with the
17 * components renumbered by decreasing size exactly as MATLAB does (a stable
18 * sort, so components of equal size keep their completion order), and a
19 * component declared recurrent when no state in it has a successor outside it.
20 *
21 * Two details of the MATLAB version are reproduced deliberately. The depth
22 * first search follows the COLUMNS of the adjacency matrix, i.e. the reversed
23 * graph, while the recurrence test follows the ROWS; the component partition
24 * is the same for a graph and its reverse, so this only affects the discovery
25 * order, but reproducing it keeps the component numbering identical when sizes
26 * tie. The recursion of the MATLAB original is replaced by an explicit stack,
27 * which visits vertices in the same order and does not overflow on the tens of
28 * thousands of states a lumped generator can carry.
29 *
30 * The computation is combinatorial: an entry only ever has its non-zero-ness
31 * tested, no arithmetic is performed, so it is exact at every number type.
32 */
33
34#include <algorithm>
35#include <cstddef>
36#include <numeric>
37#include <vector>
38
39#include "line/num/number.h"
40#include "line/util/error.h"
41#include "line/util/matrix.h"
42
43namespace line {
44namespace mc {
45
46struct SccResult {
47 /** Component index of each state, 1-based as in MATLAB (0 is never used). */
48 std::vector<std::size_t> scc;
49 /** recurrent[c-1] is true when component c has no edge leaving it. */
50 std::vector<bool> recurrent;
51 /** Member states of each component, ascending. */
52 std::vector<std::vector<std::size_t>> members;
53 std::size_t numSCC() const { return members.size(); }
54};
55
56/**
57 * @brief Strongly connected components of a directed graph, and which of them
58 * are recurrent (closed under the successor relation).
59 *
60 * @param A adjacency matrix; an edge i -> j exists iff A(i,j) is non-zero
61 */
62template <class T>
64 const std::size_t n = A.rows();
65 if (A.cols() != n) throw InputError("stronglyconncomp: adjacency matrix is not square");
66 const T zero = num_traits<T>::from_int(0);
67
68 // Successor and predecessor lists. The search runs on predecessors (the
69 // MATLAB find(e(:,i))), the recurrence test on successors.
70 std::vector<std::vector<std::size_t>> pred(n), succ(n);
71 for (std::size_t i = 0; i < n; ++i)
72 for (std::size_t j = 0; j < n; ++j)
73 if (A(i, j) != zero) {
74 succ[i].push_back(j);
75 pred[j].push_back(i);
76 }
77
78 std::vector<std::size_t> index(n, 0), low(n, 0);
79 std::vector<char> onstack(n, 0);
80 std::vector<std::size_t> stack;
81 std::vector<std::vector<std::size_t>> comps;
82 std::size_t counter = 0;
83
84 // Explicit emulation of the recursive Tarjan: each frame is a vertex and
85 // the position reached in its predecessor list.
86 std::vector<std::pair<std::size_t, std::size_t>> frames;
87 for (std::size_t s = 0; s < n; ++s) {
88 if (index[s] != 0) continue;
89 frames.push_back(std::make_pair(s, static_cast<std::size_t>(0)));
90 ++counter;
91 index[s] = counter;
92 low[s] = counter;
93 stack.push_back(s);
94 onstack[s] = 1;
95 while (!frames.empty()) {
96 const std::size_t u = frames.back().first;
97 std::size_t& p = frames.back().second;
98 if (p < pred[u].size()) {
99 const std::size_t v = pred[u][p];
100 ++p;
101 if (index[v] == 0) {
102 ++counter;
103 index[v] = counter;
104 low[v] = counter;
105 stack.push_back(v);
106 onstack[v] = 1;
107 frames.push_back(std::make_pair(v, static_cast<std::size_t>(0)));
108 } else if (onstack[v]) {
109 if (index[v] < low[u]) low[u] = index[v];
110 }
111 continue;
112 }
113 // u is finished: close a component or propagate its low link.
114 if (low[u] == index[u]) {
115 std::vector<std::size_t> comp;
116 for (;;) {
117 const std::size_t w = stack.back();
118 stack.pop_back();
119 onstack[w] = 0;
120 comp.push_back(w);
121 if (w == u) break;
122 }
123 std::sort(comp.begin(), comp.end());
124 comps.push_back(comp);
125 }
126 frames.pop_back();
127 if (!frames.empty()) {
128 const std::size_t parent = frames.back().first;
129 if (low[u] < low[parent]) low[parent] = low[u];
130 }
131 }
132 }
133
134 // Renumber by decreasing size, stably, as MATLAB's sort(...,'descend') does.
135 std::vector<std::size_t> order(comps.size());
136 std::iota(order.begin(), order.end(), static_cast<std::size_t>(0));
137 std::stable_sort(order.begin(), order.end(),
138 [&comps](std::size_t a, std::size_t b) {
139 return comps[a].size() > comps[b].size();
140 });
141
142 SccResult r;
143 r.members.resize(comps.size());
144 r.scc.assign(n, 0);
145 for (std::size_t k = 0; k < order.size(); ++k) {
146 r.members[k] = comps[order[k]];
147 for (std::size_t v : r.members[k]) r.scc[v] = k + 1;
148 }
149
150 r.recurrent.assign(comps.size(), true);
151 for (std::size_t k = 0; k < r.members.size(); ++k) {
152 for (std::size_t v : r.members[k]) {
153 for (std::size_t w : succ[v])
154 if (r.scc[w] != k + 1) {
155 r.recurrent[k] = false;
156 break;
157 }
158 if (!r.recurrent[k]) break;
159 }
160 }
161 return r;
162}
163
164} // namespace mc
165} // namespace line
166
167#endif // LINE_API_MC_STRONGLYCONNCOMP_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
SccResult stronglyconncomp(const Matrix< T > &A)
Strongly connected components of a directed graph, and which of them are recurrent (closed under the ...
Number-type abstraction for the templated API port.
std::size_t numSCC() const
std::vector< bool > recurrent
recurrent[c-1] is true when component c has no edge leaving it.
std::vector< std::size_t > scc
Component index of each state, 1-based as in MATLAB (0 is never used).
std::vector< std::vector< std::size_t > > members
Member states of each component, ascending.