Jlm
Loading...
Searching...
No Matches
TarjanSccTests.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2024 HÃ¥vard Krogstie <krogstie.havard@gmail.com>
3 * See COPYING for terms of redistribution.
4 */
5
6#include <gtest/gtest.h>
7
10
11#include <iostream>
12#include <optional>
13#include <tuple>
14#include <vector>
15
16// Used to represent graphs with no node unification
17static size_t
18Identity(size_t i)
19{
20 return i;
21}
22
30template<typename UnificationRootFunctor, typename SuccessorFunctor>
31static void
33 size_t numNodes,
34 UnificationRootFunctor & unificationRoot,
35 SuccessorFunctor & successors,
36 size_t numSccs,
37 const std::vector<size_t> & sccIndex,
38 const std::vector<size_t> & reverseTopologicalOrder)
39{
40 EXPECT_EQ(numNodes, sccIndex.size());
41
42 // Check that all sccIndex are valid, and each SCC has at least one node
43 std::vector<size_t> numNodesInScc(numSccs, 0);
44 for (size_t i = 0; i < numNodes; i++)
45 {
46 auto node = unificationRoot(i);
47 EXPECT_LT(sccIndex[node], numSccs);
48 numNodesInScc[sccIndex[node]]++;
49 }
50 for (size_t i = 0; i < numSccs; i++)
51 EXPECT_GT(numNodesInScc[i], 0u);
52
53 // Check that no edge in the graph points to an earlier SCC
54 for (size_t i = 0; i < numNodes; i++)
55 {
56 // Only consider unification roots
57 if (unificationRoot(i) != i)
58 continue;
59
60 for (auto next : successors(i))
61 {
62 next = unificationRoot(next);
63
64 // successor SCCs must have lower scc index
65 EXPECT_LE(sccIndex[next], sccIndex[i]);
66 }
67 }
68
69 // Check that all unification roots appear exactly once in the topological order
70 jlm::util::HashSet<size_t> nodeInTopologicalOrder(
71 reverseTopologicalOrder.begin(),
72 reverseTopologicalOrder.end());
73 EXPECT_EQ(nodeInTopologicalOrder.Size(), reverseTopologicalOrder.size());
74
75 for (size_t i = 0; i < numNodes; i++)
76 {
77 if (unificationRoot(i) == i)
78 EXPECT_TRUE(nodeInTopologicalOrder.Contains(i));
79 else
80 EXPECT_TRUE(!nodeInTopologicalOrder.Contains(i));
81 }
82
83 // Check that the reverse topological order contains nodes with ascending sccIndex
84 for (size_t i = 1; i < reverseTopologicalOrder.size(); i++)
85 {
86 EXPECT_LE(sccIndex[reverseTopologicalOrder[i - 1]], sccIndex[reverseTopologicalOrder[i]]);
87 }
88}
89
90TEST(TarjanSccTests, TestDag)
91{
92 // Create a DAG, where each node is its own SCC
93 const size_t numNodes = 8;
94 std::vector<std::vector<size_t>> successors{
95 { 2 }, // 0's successors
96 { 2 }, // 1's successors
97 { 3, 4 }, // 2's successors
98 { 5, 6 }, // 3's successors
99 { 5 }, // 4's successors
100 { 6 }, // 5's successors
101 {}, // 6's successors
102 { 7 }, // 7's successors (fully disconnected, just a self-loop)
103 };
104
105 auto GetSuccessors = [&](size_t node)
106 {
107 return successors[node];
108 };
109
110 std::vector<size_t> sccIndex;
111 std::vector<size_t> reverseTopologicalOrder;
113 numNodes,
114 GetSuccessors,
115 sccIndex,
116 reverseTopologicalOrder);
118 numNodes,
119 Identity,
120 GetSuccessors,
121 numSccs,
122 sccIndex,
123 reverseTopologicalOrder);
124
125 EXPECT_EQ(numSccs, numNodes);
126}
127
128// Test a graph with some cycles, ensuring they become SCCs
129TEST(TarjanSccTests, TestCycles)
130{
131 const size_t numNodes = 7;
132 std::vector<std::vector<size_t>> successors{
133 { 1, 3 }, // 0's successors
134 { 2 }, // 1's successors
135 { 0 }, // 2's successors
136 { 4, 5 }, // 3's successors
137 { 0, 5 }, // 4's successors
138 {}, // 5's successors
139 { 2, 4, 5 }, // 6's successors
140 };
141
142 auto GetSuccessors = [&](size_t node)
143 {
144 return successors[node];
145 };
146
147 std::vector<size_t> sccIndex;
148 std::vector<size_t> reverseTopologicalOrder;
150 numNodes,
151 GetSuccessors,
152 sccIndex,
153 reverseTopologicalOrder);
155 numNodes,
156 Identity,
157 GetSuccessors,
158 numSccs,
159 sccIndex,
160 reverseTopologicalOrder);
161
162 EXPECT_EQ(numSccs, 3u);
163 // 5 has to be at the end
164 EXPECT_EQ(sccIndex[5], 0u);
165 EXPECT_EQ(reverseTopologicalOrder[0], 5u);
166 // 6 has to be at the beginning
167 EXPECT_EQ(sccIndex[6], 2u);
168 EXPECT_EQ(reverseTopologicalOrder[numNodes - 1], 6u);
169 // The rest belong to the middle SCC
170 for (size_t i = 0; i < 5; i++)
171 EXPECT_EQ(sccIndex[i], 1u);
172}
173
194static std::tuple<size_t, size_t, std::vector<size_t>>
195CreateDiamondChain(size_t knots, std::optional<std::pair<size_t, size_t>> extraEdge)
196{
197 EXPECT_GE(knots, 2u);
198 const size_t numNodes = 3 * knots - 2;
199 std::vector<std::vector<size_t>> successors(numNodes);
200
201 // Use a permutation to "randomize" the node indices
202 std::vector<size_t> perm(numNodes);
203 for (size_t i = 0; i < numNodes; i++)
204 {
205 // 3 and numNodes is always coprime, so this will assign each node a unique new index
206 perm[i] = (i * 3) % numNodes;
207 }
208
209 for (size_t knot = 0; knot + 3 < numNodes; knot += 3)
210 {
211 successors[perm[knot]].push_back(perm[knot + 1]);
212 successors[perm[knot]].push_back(perm[knot + 2]);
213 successors[perm[knot + 1]].push_back(perm[knot + 3]);
214 successors[perm[knot + 2]].push_back(perm[knot + 3]);
215 }
216
217 if (extraEdge)
218 {
219 EXPECT_LT(extraEdge->first, numNodes);
220 EXPECT_LT(extraEdge->second, numNodes);
221 // Leave 5 nodes on the end, and 5 at the beginning, out of the big loop
222 successors[perm[extraEdge->first]].push_back(perm[extraEdge->second]);
223 }
224
225 auto GetSuccessors = [&](size_t node)
226 {
227 return successors[node];
228 };
229
230 std::vector<size_t> sccIndex;
231 std::vector<size_t> reverseTopologicalOrder;
233 numNodes,
234 GetSuccessors,
235 sccIndex,
236 reverseTopologicalOrder);
238 numNodes,
239 Identity,
240 GetSuccessors,
241 numSccs,
242 sccIndex,
243 reverseTopologicalOrder);
244
245 std::vector<size_t> unshuffledNodeIndex(numNodes);
246 for (size_t i = 0; i < numNodes; i++)
247 {
248 unshuffledNodeIndex[i] = sccIndex[perm[i]];
249 }
250
251 return { numNodes, numSccs, std::move(unshuffledNodeIndex) };
252}
253
254TEST(TarjanSccTests, TestSimpleDiamondChain)
255{
256 auto [numNodes, numSccs, sccIndex] = CreateDiamondChain(100, std::nullopt);
257 EXPECT_EQ(numNodes, numSccs);
258}
259
260TEST(TarjanSccTests, TestDiamondChainWithForwardEdge)
261{
262 // Forward edges do not create any cycles
263 std::pair<size_t, size_t> forwardEdge{ 5, 200 };
264 auto [numNodes, numSccs, sccIndex] = CreateDiamondChain(100, forwardEdge);
265 EXPECT_EQ(numNodes, numSccs);
266}
267
268TEST(TarjanSccTests, TestDiamondChainWithBackEdge)
269{
270 // Back edges create one big SCC, the rest are single node SCCs
271 std::pair<size_t, size_t> backEdge{ 3 * 50, 3 * 3 };
272 auto [numNodes, numSccs, sccIndex] = CreateDiamondChain(100, backEdge);
273
274 std::cerr << "numSccs: " << numSccs << std::endl;
275 std::cerr << "numNodes: " << numNodes << std::endl;
276 EXPECT_EQ(numSccs, backEdge.second + numNodes - backEdge.first);
277
278 auto largeScc = sccIndex[backEdge.second];
279
280 // All nodes before the back edge head have a higher SCC index
281 for (size_t i = 0; i < backEdge.second; i++)
282 EXPECT_GT(sccIndex[i], largeScc);
283
284 // All nodes after the back edge tail have a lower SCC index
285 for (size_t i = backEdge.first + 1; i < numNodes; i++)
286 EXPECT_LT(sccIndex[i], largeScc);
287
288 // All nodes between the back edge tail and head have same SCC index
289 for (size_t i = backEdge.second; i <= backEdge.first; i++)
290 EXPECT_EQ(sccIndex[i], largeScc);
291}
292
293// During SCC creation, the function should query a node for its successors at most twice
294TEST(TarjanSccTests, TestVisitEachNodeTwice)
295{
296 const size_t numNodes = 5;
297 std::vector<std::vector<size_t>> successors{
298 { 1, 2 }, // 0's successors
299 { 2, 3 }, // 1's successors
300 { 1, 3 }, // 2's successors
301 {}, // 3's successors
302 { 4 } // 4's successors
303 };
304 // The graph looks like a 0->(12)->3 diamond with edges between 1 and 2, and a lone node 4
305
306 std::vector<size_t> successorsQueried(numNodes, 0);
307 auto GetSuccessors = [&](size_t node)
308 {
309 EXPECT_LT(node, numNodes);
310 successorsQueried[node]++;
311 return successors[node];
312 };
313
314 std::vector<size_t> sccIndex;
315 std::vector<size_t> reverseTopologicalOrder;
317 numNodes,
318 GetSuccessors,
319 sccIndex,
320 reverseTopologicalOrder);
321
322 EXPECT_EQ(numSccs, 4u);
323 for (size_t timesQueried : successorsQueried)
324 EXPECT_LE(timesQueried, 2u);
325
326 // Validate the produced SCC DAG as well, but do it last, as this function calls GetSuccessors.
328 numNodes,
329 Identity,
330 GetSuccessors,
331 numSccs,
332 sccIndex,
333 reverseTopologicalOrder);
334}
335
336TEST(TarjanSccTests, TestUnifiedNodes)
337{
338 // Each node with index >= 5 has a unification root equal to index - 5
339 const size_t numNodes = 10;
340 std::vector<std::vector<size_t>> successors{
341 { 1, 1 + 5, 2 }, // 0's successors
342 { 2 + 5, 3 }, // 1's successors
343 { 1 + 5, 3 + 5 }, // 2's successors
344 {}, // 3's successors
345 { 4, 4 + 5 } // 4's successors
346 };
347 // The graph looks like a 0->(12)->3 diamond with edges between 1 and 2, and a lone node 4
348
349 auto GetUnificationRoot = [&](size_t node)
350 {
351 if (node >= 5)
352 return node - 5;
353 return node;
354 };
355
356 auto GetSuccessors = [&](size_t node)
357 {
358 EXPECT_LT(node, 5u);
359 return successors[node];
360 };
361
362 std::vector<size_t> sccIndex;
363 std::vector<size_t> reverseTopologicalOrder;
365 numNodes,
366 GetUnificationRoot,
367 GetSuccessors,
368 sccIndex,
369 reverseTopologicalOrder);
370
371 EXPECT_EQ(numSccs, 4u);
372
373 // Validate the produced SCC DAG as well, but do it last, as this function calls GetSuccessors.
375 numNodes,
376 GetUnificationRoot,
377 GetSuccessors,
378 numSccs,
379 sccIndex,
380 reverseTopologicalOrder);
381}
static void ValidateTopologicalOrderAndSccIndices(size_t numNodes, UnificationRootFunctor &unificationRoot, SuccessorFunctor &successors, size_t numSccs, const std::vector< size_t > &sccIndex, const std::vector< size_t > &reverseTopologicalOrder)
TEST(TarjanSccTests, TestDag)
static std::tuple< size_t, size_t, std::vector< size_t > > CreateDiamondChain(size_t knots, std::optional< std::pair< size_t, size_t > > extraEdge)
static size_t Identity(size_t i)
std::size_t Size() const noexcept
Definition HashSet.hpp:187
bool Contains(const ItemType &item) const noexcept
Definition HashSet.hpp:150
NodeType FindStronglyConnectedComponents(NodeType numNodes, UnificationRootFunctor unificationRoot, SuccessorFunctor &successors, std::vector< NodeType > &sccIndex, std::vector< NodeType > &reverseTopologicalOrder)
Definition TarjanScc.hpp:52