Skip to content

Commit d2d7594

Browse files
Ilya Yaroshenko9il
authored andcommitted
add mir.graph.tarjan
1 parent 352e17c commit d2d7594

File tree

8 files changed

+637
-29
lines changed

8 files changed

+637
-29
lines changed

source/mir/functional.d

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ import mir.math.common: optmath;
5252

5353
@optmath:
5454

55+
/++
56+
Constructs static array.
57+
+/
58+
T[N] staticArray(T, size_t N)(T[N] a...)
59+
{
60+
return a;
61+
}
62+
5563
/++
5664
Simple wrapper that holds a pointer.
5765
It is used for as workaround to return multiple auto ref values.

source/mir/graph/tarjan.d

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
/++
2+
Tarjan's strongly connected components algorithm.
3+
4+
License: $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0).
5+
Copyright: Copyright © 2018-, Kaleidic Associates Advisory Limited
6+
Authors: Ilya Yaroshenko
7+
8+
Macros:
9+
SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, ndslice, $1)$(NBSP)
10+
T2=$(TR $(TDNW $(LREF $1)) $(TD $+))
11+
+/
12+
13+
module mir.graph.tarjan;
14+
15+
import std.traits;
16+
17+
/++
18+
Tarjan's strongly connected components algorithm.
19+
20+
Tarjan's algorithm is an algorithm in graph theory for finding the strongly connected components of a graph.
21+
It runs in linear time, matching the time bound for alternative methods including Kosaraju's algorithm and the path-based strong component algorithm.
22+
23+
Complexity: worst-case `O(|V| + |E|)`.
24+
25+
Params:
26+
graph = random access range of random accees ranges of nodes indeces
27+
Returns:
28+
components (ndslice of arrays of indexes)
29+
30+
See_also:
31+
$(SUBREF utility, graph)
32+
+/
33+
pragma(inline, false)
34+
auto tarjan(G, I = Unqual!(ForeachType!(ForeachType!G)))(G graph)
35+
if (isUnsigned!I)
36+
{
37+
import mir.utility: min;
38+
39+
static if (I.sizeof >= uint.sizeof)
40+
alias S = size_t;
41+
else
42+
alias S = uint;
43+
44+
enum undefined = I.max;
45+
46+
static struct IndexNode
47+
{
48+
I index;
49+
I lowlink;
50+
51+
@property:
52+
53+
bool isRoot()()
54+
{
55+
return index == lowlink;
56+
}
57+
58+
bool isUndefined()()
59+
{
60+
return index == undefined;
61+
}
62+
}
63+
64+
static struct LoopNode
65+
{
66+
union
67+
{
68+
struct
69+
{
70+
I i;
71+
I j;
72+
}
73+
S index;
74+
}
75+
}
76+
77+
bool[] onStack = new bool[graph.length];
78+
I[] stack;
79+
IndexNode[] indeces;
80+
LoopNode[] loopStack;
81+
I index;
82+
sizediff_t stackIndex;
83+
sizediff_t backStackIndex = graph.length;
84+
sizediff_t componentBackStackIndex = graph.length + 1;
85+
86+
if (__ctfe)
87+
{
88+
stack = new I[graph.length];
89+
indeces = new IndexNode[graph.length];
90+
loopStack = new LoopNode[componentBackStackIndex];
91+
}
92+
else
93+
{
94+
() @trusted {
95+
import std.array: uninitializedArray;
96+
97+
stack = uninitializedArray!(I[])(graph.length);
98+
indeces = uninitializedArray!(IndexNode[])(graph.length);
99+
loopStack = uninitializedArray!(LoopNode[])(componentBackStackIndex);
100+
} ();
101+
}
102+
103+
foreach(ref node; indeces)
104+
node.index = undefined;
105+
106+
foreach(size_t v; 0u .. graph.length)
107+
{
108+
if (indeces[v].isUndefined)
109+
{
110+
sizediff_t loopStackIndex;
111+
loop:
112+
// Set the depth index for v to the smallest unused index
113+
indeces[v].index = cast(I) index;
114+
indeces[v].lowlink = cast(I) index;
115+
index++;
116+
stack[stackIndex++] = cast(I) v;
117+
onStack[v] = true;
118+
119+
// Consider successors of v
120+
auto e = graph[v];
121+
I w;
122+
size_t wi;
123+
124+
for (; wi < e.length; wi++)
125+
{
126+
w = e[wi];
127+
if (onStack[w])
128+
{
129+
// Successor w is in stack S and hence in the current SCC
130+
// If w is not on stack, then (v, w) is a cross-edge in the DFS tree and must be ignored
131+
// Note: The next line may look odd - but is correct.
132+
// It says w.index not w.lowlink; that is deliberate and from the original paper
133+
indeces[v].lowlink = min(indeces[v].lowlink, indeces[w].index);
134+
continue;
135+
}
136+
if (indeces[w].isUndefined)
137+
{
138+
// Successor w has not yet been visited; recurse on it
139+
// strongconnect(w)
140+
assert(loopStackIndex < loopStack.length);
141+
loopStack[loopStackIndex] = LoopNode(cast(I) v, cast(I) wi);
142+
++loopStackIndex;
143+
assert(componentBackStackIndex > loopStackIndex);
144+
v = e[wi];
145+
goto loop;
146+
retRec:
147+
v = loopStack[loopStackIndex].i;
148+
wi = loopStack[loopStackIndex].j;
149+
e = graph[v];
150+
w = e[wi];
151+
indeces[v].lowlink = min(indeces[v].lowlink, indeces[w].lowlink);
152+
}
153+
}
154+
155+
// If v is a root node, pop the stack and generate an SCC
156+
if (indeces[v].isRoot)
157+
{
158+
// start a new strongly connected component
159+
do
160+
{
161+
assert(stackIndex > 0);
162+
assert(backStackIndex > 0);
163+
// add w to current strongly connected component
164+
--backStackIndex;
165+
--stackIndex;
166+
w = stack[backStackIndex] = stack[stackIndex];
167+
onStack[w] = false;
168+
}
169+
while (w != v);
170+
171+
// output the current strongly connected component
172+
assert(componentBackStackIndex > loopStackIndex);
173+
--componentBackStackIndex;
174+
loopStack[componentBackStackIndex].index = cast(S) backStackIndex;
175+
}
176+
if (--loopStackIndex >= 0)
177+
goto retRec;
178+
}
179+
}
180+
181+
S[] pairwiseIndex;
182+
if (__ctfe)
183+
{
184+
pairwiseIndex = new S[graph.length - componentBackStackIndex + 1];
185+
}
186+
else
187+
{
188+
() @trusted {
189+
import std.array: uninitializedArray;
190+
pairwiseIndex = uninitializedArray!(S[])(graph.length + 1 - componentBackStackIndex + 1);
191+
} ();
192+
}
193+
foreach (i, ref e; loopStack[componentBackStackIndex .. $])
194+
{
195+
pairwiseIndex[i] = e.index;
196+
}
197+
pairwiseIndex[$ - 1] = cast(I) graph.length;
198+
199+
import mir.ndslice.slice: sliced;
200+
import mir.ndslice.topology: pairwiseMapSubSlices;
201+
return pairwiseIndex.sliced.pairwiseMapSubSlices(()@trusted {return stack.ptr; }());
202+
}
203+
204+
/++
205+
------
206+
4 <- 5 <- 6 -------> 7 -> 8 -> 11
207+
\ ^ ^ ^ \
208+
v \ \ \ \
209+
0 -> 1 -> 2 -> 3 -> 10 9 <---
210+
------
211+
+/
212+
@safe pure version(mir_test) unittest
213+
{
214+
import mir.graph.utility;
215+
import mir.ndslice.algorithm: each;
216+
import mir.ndslice.sorting: sort;
217+
import std.array: array;
218+
219+
GraphSeries!(string, uint) gs = [
220+
"00": ["01"],
221+
"01": ["02"],
222+
"02": ["03", "05"],
223+
"03": ["06", "10"],
224+
"04": ["01"],
225+
"05": ["04"],
226+
"06": ["05", "07"],
227+
"07": ["08"],
228+
"08": ["09", "11"],
229+
"09": ["07"],
230+
"10": [],
231+
"11": [],
232+
].graphSeries;
233+
234+
auto components = gs.data.tarjan;
235+
components.each!sort; // sort indexes in each component
236+
237+
assert(components.array.sort == [
238+
[0u],
239+
[1u, 2, 3, 4, 5, 6],
240+
[7u, 8, 9],
241+
[10u],
242+
[11u],
243+
]);
244+
}
245+
246+
/++
247+
Tests that the graph `0 -> 1 -> 2 -> 3 -> 4`` returns 4 components.
248+
+/
249+
@safe pure version(mir_test) unittest
250+
{
251+
import mir.graph.utility;
252+
253+
GraphSeries!(char, uint) gs = [
254+
'a': ['b'],
255+
'b': ['c'],
256+
'c': ['d'],
257+
'd': ['q'],
258+
'q': [],
259+
].graphSeries;
260+
261+
auto scc = gs.data.tarjan;
262+
263+
assert(scc.length == 5);
264+
265+
foreach(uint[] component; scc)
266+
assert(component.length == 1);
267+
268+
assert(scc == [[0], [1], [2], [3], [4]]);
269+
}
270+
271+
/++
272+
----
273+
0 <- 2 <-- 5 <--> 6
274+
\ ^ ^ ^ ^
275+
v / \ \ \
276+
1 <- 3 <-> 4 <-- 7 <--(links to self)
277+
----
278+
+/
279+
@safe pure version(mir_test) unittest
280+
{
281+
import mir.graph.utility;
282+
import mir.ndslice.algorithm: each;
283+
import mir.ndslice.sorting: sort;
284+
import std.array: array;
285+
286+
auto gs = [
287+
0: [1],
288+
1: [2],
289+
2: [0],
290+
3: [1, 2, 4],
291+
4: [3, 2],
292+
5: [2, 6],
293+
6: [5],
294+
7: [4, 7],
295+
].graphSeries;
296+
297+
auto components = gs.data.tarjan;
298+
components.each!sort; // sort indexes in each component
299+
300+
assert(components.array.sort == [
301+
[0, 1, 2],
302+
[3, 4],
303+
[5, 6],
304+
[7]
305+
]);
306+
}
307+
308+
/++
309+
-----
310+
2 <-> 1
311+
\ ^
312+
v /
313+
0
314+
-----
315+
+/
316+
@safe pure version(mir_test) unittest
317+
{
318+
import mir.graph.utility;
319+
import mir.ndslice.algorithm: each;
320+
import mir.ndslice.sorting: sort;
321+
import std.array: array;
322+
323+
auto gs = [
324+
0: [1],
325+
1: [2],
326+
2: [0, 1],
327+
].graphSeries;
328+
329+
auto components = gs.data.tarjan;
330+
components.each!sort; // sort indexes in each component
331+
332+
assert(components == [[0, 1, 2]]);
333+
}
334+
335+
/++
336+
Tests that a strongly connected graph, where components have
337+
to get through previously visited components to get to the
338+
graph root works properly
339+
340+
This test demonstrates a hard to detect bug, where vertices
341+
were being marked 'off-stack' after they were first visited,
342+
not when they were actually removed from the stack
343+
+/
344+
@safe pure version(mir_test) unittest
345+
{
346+
import mir.graph.utility;
347+
import mir.ndslice.algorithm: each;
348+
import mir.ndslice.sorting: sort;
349+
import std.array: array;
350+
351+
auto root = 0;
352+
auto lvl1 = [1,2,3,4,5,6,7,8,9,10];
353+
auto lvl2 = [11,12,13,14,15,16,17,18,19,20];
354+
355+
int[][int] aar;
356+
aar[root] = lvl1;
357+
foreach(int v; lvl1)
358+
aar[v] = lvl2;
359+
foreach(int v; lvl2)
360+
aar[v] = [root];
361+
362+
auto gs = aar.graphSeries;
363+
364+
auto components = gs.data.tarjan;
365+
components.each!sort; // sort indexes in each component
366+
367+
assert(components == [root ~ lvl1 ~ lvl2]);
368+
}

0 commit comments

Comments
 (0)