Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
k_shortest_paths_test.cc
Go to the documentation of this file.
1/*
2 Aleph_w
3
4 Data structures & Algorithms
5 version 2.0.0b
6 https://github.com/lrleon/Aleph-w
7
8 This file is part of Aleph-w library
9
10 Copyright (c) 2002-2026 Leandro Rabindranath Leon
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29*/
30
31
37# include <gtest/gtest.h>
38
39# include <algorithm>
40# include <chrono>
41# include <cstdlib>
42# include <functional>
43# include <limits>
44# include <random>
45# include <string>
46# include <tuple>
47# include <utility>
48
49# include <K_Shortest_Paths.H>
50# include <tpl_array.H>
51# include <tpl_dynArray.H>
52# include <tpl_graph.H>
53# include <tpl_olhash.H>
54
55using namespace Aleph;
56
57namespace
58{
60 using Node = Graph::Node;
61 using Arc = Graph::Arc;
62 using Edge_Def = std::tuple<size_t, size_t, long long>;
63
64 struct Canonical_Path
65 {
66 long long cost = 0;
68
69 bool operator==(const Canonical_Path & other) const noexcept
70 {
71 if (cost != other.cost or nodes.size() != other.nodes.size())
72 return false;
73
74 for (size_t i = 0; i < nodes.size(); ++i)
75 if (nodes(i) != other.nodes(i))
76 return false;
77
78 return true;
79 }
80 };
81
82
83 bool same_node_sequence(const DynArray<int> & a, const DynArray<int> & b)
84 {
85 if (a.size() != b.size())
86 return false;
87
88 for (size_t i = 0; i < a.size(); ++i)
89 if (a(i) != b(i))
90 return false;
91
92 return true;
93 }
94
95
96 bool node_sequence_less(const DynArray<int> & a, const DynArray<int> & b)
97 {
98 const size_t min_size = std::min(a.size(), b.size());
99 for (size_t i = 0; i < min_size; ++i)
100 {
101 if (a(i) < b(i))
102 return true;
103 if (a(i) > b(i))
104 return false;
105 }
106
107 return a.size() < b.size();
108 }
109
110
111 bool canonical_less(const Canonical_Path & a, const Canonical_Path & b)
112 {
113 if (a.cost != b.cost)
114 return a.cost < b.cost;
115 return node_sequence_less(a.nodes, b.nodes);
116 }
117
118
120 {
121 for (size_t i = 1; i < paths.size(); ++i)
122 {
123 Canonical_Path key = paths[i];
124 size_t j = i;
125 while (j > 0 and canonical_less(key, paths[j - 1]))
126 {
127 paths[j] = paths[j - 1];
128 --j;
129 }
130 paths[j] = key;
131 }
132 }
133
134
136 const Canonical_Path & needle)
137 {
138 for (size_t i = 0; i < paths.size(); ++i)
139 if (paths[i] == needle)
140 return true;
141 return false;
142 }
143
144
145 Array<Edge_Def> make_edges(std::initializer_list<Edge_Def> init)
146 {
147 Array<Edge_Def> edges;
148 edges.reserve(init.size());
149 for (const auto & e : init)
150 edges.append(e);
151 return edges;
152 }
153
154
155 struct Built_Graph
156 {
157 Graph g;
159 };
160
161
162 Built_Graph build_graph(const size_t n, const Array<Edge_Def> & edges)
163 {
164 Built_Graph built;
165 built.nodes.reserve(n);
166 for (size_t i = 0; i < n; ++i)
167 built.nodes.append(built.g.insert_node(static_cast<int>(i)));
168
169 for (typename Array<Edge_Def>::Iterator it(edges); it.has_curr(); it.next_ne())
170 {
171 const auto & [u, v, w] = it.get_curr();
172 if (u < n and v < n)
173 built.g.insert_arc(built.nodes[u], built.nodes[v], w);
174 }
175
176 return built;
177 }
178
179
181 {
183 for (Path<Graph>::Iterator it(path); it.has_current_node(); it.next_ne())
184 ids.append(it.get_current_node_ne()->get_info());
185 return ids;
186 }
187
188
189 bool is_simple_path(const Path<Graph> & path)
190 {
192 for (Path<Graph>::Iterator it(path); it.has_current_node(); it.next_ne())
193 {
194 Node * node = it.get_current_node_ne();
195 if (seen.contains(node))
196 return false;
197 seen.insert(node);
198 }
199 return true;
200 }
201
202
203 template <class Result_List>
205 {
207 for (typename Result_List::Iterator it(results); it.has_curr(); it.next_ne())
208 {
209 const auto & item = it.get_curr();
210 out.append(Canonical_Path{
211 item.total_cost,
212 extract_node_ids(item.path)});
213 }
214
216 return out;
217 }
218
219
222 Node * target,
223 const size_t k)
224 {
225 if (k == 0)
226 return Array<Canonical_Path>();
227
228 if (source == target)
229 {
230 Canonical_Path p;
231 p.cost = 0;
232 p.nodes.append(source->get_info());
234 out.append(p);
235 return out;
236 }
237
239 OLhashTable<Node *> visited;
240 DynArray<int> stack;
241
242 std::function<void(Node *, long long)> dfs =
243 [&](Node * u, const long long cost)
244 {
245 if (u == target)
246 {
247 Canonical_Path p;
248 p.cost = cost;
249 for (size_t i = 0; i < stack.size(); ++i)
250 p.nodes.append(stack(i));
251 all.append(p);
252 return;
253 }
254
255 for (Node_Arc_Iterator<Graph> it(u); it.has_curr(); it.next_ne())
256 {
257 Arc * arc = it.get_current_arc_ne();
258 Node * v = it.get_tgt_node();
259 if (visited.contains(v))
260 continue;
261
262 visited.insert(v);
263 stack.append(v->get_info());
264 dfs(v, cost + arc->get_info());
265 (void) stack.pop();
266 visited.remove(v);
267 }
268 };
269
270 visited.insert(source);
271 stack.append(source->get_info());
272 dfs(source, 0);
273
275
276 if (all.size() > k)
277 while (all.size() > k)
278 (void) all.remove_last();
279 return all;
280 }
281
282
283 Array<Edge_Def> random_graph_edges(std::mt19937_64 & rng,
284 const size_t n)
285 {
286 std::bernoulli_distribution has_edge(0.36);
287 std::uniform_int_distribution<int> base_w(1, 40);
288
289 Array<Edge_Def> edges;
290 size_t edge_id = 0;
291 for (size_t u = 0; u < n; ++u)
292 for (size_t v = 0; v < n; ++v)
293 {
294 if (u == v or not has_edge(rng))
295 continue;
296 const long long w = static_cast<long long>(base_w(rng))
297 + static_cast<long long>(edge_id * 101);
298 edges.append(Edge_Def{u, v, w});
299 ++edge_id;
300 }
301 return edges;
302 }
303} // namespace
304
305
307{
308 const auto edges = make_edges({
309 {0, 1, 1}, {0, 2, 1}, {1, 3, 1}, {2, 3, 1},
310 {1, 2, 1}, {2, 1, 1}, {0, 3, 5}
311 });
312 auto built = build_graph(4, edges);
313
314 const auto got = yen_k_shortest_paths<Graph>(
315 built.g, built.nodes[0], built.nodes[3], 5);
317
318 ASSERT_EQ(normalized.size(), 5u);
319 EXPECT_EQ(normalized[0].cost, 2);
320 EXPECT_EQ(normalized[1].cost, 2);
321 EXPECT_EQ(normalized[2].cost, 3);
322 EXPECT_EQ(normalized[3].cost, 3);
323 EXPECT_EQ(normalized[4].cost, 5);
324}
325
326
328{
329 const auto edges = make_edges({
330 {0, 1, 1}, {0, 2, 1}, {1, 3, 1}, {2, 3, 1},
331 {1, 2, 2}, {0, 3, 7}
332 });
333 auto built = build_graph(4, edges);
334
335 const auto yen = normalize_results(
336 yen_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[3], 6));
337 const auto epp = normalize_results(
338 eppstein_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[3], 6));
339
340 ASSERT_GE(epp.size(), yen.size());
341
342 for (size_t i = 0; i < yen.size(); ++i)
343 {
345 }
346}
347
348
350{
351 const auto edges = make_edges({
352 {0, 1, 1}, {1, 2, 1}, {2, 3, 1},
353 {2, 4, 1}, {4, 2, 1}
354 });
355 auto built = build_graph(5, edges);
356
357 const auto yen = normalize_results(
358 yen_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[3], 4));
359 const auto epp = normalize_results(
360 eppstein_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[3], 4));
361
362 ASSERT_EQ(yen.size(), 1u);
363 ASSERT_EQ(epp.size(), 4u);
364
365 EXPECT_EQ(epp[0].cost, 3);
366 EXPECT_EQ(epp[1].cost, 5);
367 EXPECT_EQ(epp[2].cost, 7);
368 EXPECT_EQ(epp[3].cost, 9);
369
371}
372
373
375{
376 const auto edges = make_edges({
377 {0, 1, 2}, {1, 2, 2}, {2, 0, 2}
378 });
379 auto built = build_graph(3, edges);
380
381 const auto got = yen_k_shortest_paths<Graph>(
382 built.g, built.nodes[1], built.nodes[1], 10);
383
384 ASSERT_EQ(got.size(), 1u);
385 const auto normalized = normalize_results(got);
386 ASSERT_EQ(normalized.size(), 1u);
387 EXPECT_EQ(normalized[0].cost, 0);
388 ASSERT_EQ(normalized[0].nodes.size(), 1u);
389 EXPECT_EQ(normalized[0].nodes(0), 1);
390}
391
392
394{
395 const auto edges = make_edges({
396 {0, 1, 1}, {1, 0, 1}, {2, 3, 1}
397 });
398 auto built = build_graph(4, edges);
399
400 const auto got = yen_k_shortest_paths<Graph>(
401 built.g, built.nodes[0], built.nodes[3], 4);
402 EXPECT_TRUE(got.is_empty());
403}
404
405
407{
408 const auto edges = make_edges({
409 {0, 1, 2}, {1, 2, -5}, {0, 2, 9}
410 });
411 auto built = build_graph(3, edges);
412
414 (yen_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[2], 3)),
415 std::domain_error);
417 (eppstein_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[2], 3)),
418 std::domain_error);
419}
420
421
423{
424 std::mt19937_64 rng(0xBADC0FFEEULL);
425 std::uniform_int_distribution<int> n_dist(3, 8);
426
427 for (size_t trial = 0; trial < 80; ++trial)
428 {
429 const size_t n = static_cast<size_t>(n_dist(rng));
431 Node * source = built.nodes[0];
432 Node * target = built.nodes[n - 1];
433 constexpr size_t K = 7;
434
436 source, target, K);
437
438 const auto yen = normalize_results(
439 yen_k_shortest_paths<Graph>(built.g, source, target, K));
440 const auto epp = normalize_results(
441 eppstein_k_shortest_paths<Graph>(built.g, source, target, K));
442
443 ASSERT_EQ(yen.size(), expected.size()) << "trial=" << trial;
444 ASSERT_GE(epp.size(), expected.size()) << "trial=" << trial;
445 ASSERT_LE(epp.size(), K) << "trial=" << trial;
446
447 for (size_t i = 0; i < expected.size(); ++i)
448 {
449 EXPECT_EQ(yen[i].cost, expected[i].cost)
450 << "trial=" << trial << ", i=" << i;
452 << "trial=" << trial << ", i=" << i;
453 }
454
455 if (expected.size() > 0)
456 EXPECT_EQ(epp[0].cost, expected[0].cost)
457 << "trial=" << trial;
458
459 for (size_t i = 1; i < epp.size(); ++i)
460 {
461 EXPECT_GE(epp[i].cost, epp[i - 1].cost)
462 << "trial=" << trial << ", i=" << i;
463 }
464 }
465}
466
467
469{
470 const auto edges = make_edges({
471 {0, 1, 1}, {0, 2, 4}, {1, 2, 1}, {1, 3, 2},
472 {2, 3, 1}, {2, 4, 3}, {3, 4, 1}
473 });
474 auto built = build_graph(5, edges);
475
476 const auto got = yen_k_shortest_paths<Graph>(
477 built.g, built.nodes[0], built.nodes[4], 10);
478
479 long long prev = std::numeric_limits<long long>::min();
480 for (auto it = got.get_it(); it.has_curr(); it.next_ne())
481 {
482 const auto & item = it.get_curr();
483 EXPECT_TRUE(is_simple_path(item.path));
484 EXPECT_GE(item.total_cost, prev);
485 prev = item.total_cost;
486 }
487}
488
489
491{
492 const auto edges = make_edges({
493 {0, 1, 1}, {1, 2, 1}, {2, 3, 1},
494 {2, 4, 1}, {4, 2, 1}
495 });
496 auto built = build_graph(5, edges);
497
499 built.g, built.nodes[0], built.nodes[3], 8);
500
501 long long prev = std::numeric_limits<long long>::min();
502 for (auto it = got.get_it(); it.has_curr(); it.next_ne())
503 {
504 const auto & item = it.get_curr();
505 EXPECT_GE(item.total_cost, prev);
506 prev = item.total_cost;
507 }
508}
509
510
512{
513 // Graph with:
514 // - Node 0 -> 1: Two parallel arcs with weights 10 and 20.
515 // - Node 1 -> 1: Self-loop with weight 5.
516 // - Node 1 -> 2: Arc with weight 5.
517 //
518 // Shortest paths from 0 to 2:
519 // 1. 0->1(cost=10)->2(cost=5) = 15
520 // 2. 0->1(cost=20)->2(cost=5) = 25
521 //
522 // The self-loop 1->1 should be ignored by Yen's (simple paths only).
523 // Eppstein's API is general (non-loopless), so a cheaper loopy path may
524 // appear before the second simple alternative.
525
526 const auto edges = make_edges({
527 {0, 1, 10}, // parallel arc 1
528 {0, 1, 20}, // parallel arc 2
529 {1, 1, 5}, // self-loop on spur node
530 {1, 2, 5}
531 });
532 auto built = build_graph(3, edges);
533
534 const size_t K = 2;
535 const auto yen_results = normalize_results(
536 yen_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[2], K));
537 const auto epp_results = normalize_results(
538 eppstein_k_shortest_paths<Graph>(built.g, built.nodes[0], built.nodes[2], K));
539
540 ASSERT_EQ(yen_results.size(), 2u);
541 ASSERT_EQ(epp_results.size(), 2u);
542
543 // Check path 1: Cost 15, nodes [0, 1, 2]
544 EXPECT_EQ(yen_results[0].cost, 15);
545 EXPECT_EQ(yen_results[0].nodes.size(), 3u);
546 EXPECT_EQ(yen_results[0].nodes(0), 0);
547 EXPECT_EQ(yen_results[0].nodes(1), 1);
548 EXPECT_EQ(yen_results[0].nodes(2), 2);
549
550 // Check path 2: Cost 25, nodes [0, 1, 2]
551 EXPECT_EQ(yen_results[1].cost, 25);
552 EXPECT_EQ(yen_results[1].nodes.size(), 3u);
553 EXPECT_EQ(yen_results[1].nodes(0), 0);
554 EXPECT_EQ(yen_results[1].nodes(1), 1);
555 EXPECT_EQ(yen_results[1].nodes(2), 2);
556
557 // Eppstein: first path matches Yen's best simple path.
559
560 // Eppstein can use the self-loop:
561 // 0->1(cost=10)->1(cost=5)->2(cost=5) = 20
562 EXPECT_EQ(epp_results[1].cost, 20);
563 EXPECT_EQ(epp_results[1].nodes.size(), 4u);
564 EXPECT_EQ(epp_results[1].nodes(0), 0);
565 EXPECT_EQ(epp_results[1].nodes(1), 1);
566 EXPECT_EQ(epp_results[1].nodes(2), 1);
567 EXPECT_EQ(epp_results[1].nodes(3), 2);
568}
569
570
571// ============================================================================
572// Opt-in deterministic performance regression test.
573//
574// Activate with: PERF_TESTS_ENABLED=1 ./build/Tests/k_shortest_paths_test
575//
576// Parameters (pinned for reproducibility; update the comment if you change them):
577// MASTER_SEED 0xDEADBEEFCAFEBABEULL — drives all graph generation
578// N 40 nodes per graph (vs. 3–8 in RandomSmallGraphsMatchExactOracle)
579// K 25 shortest paths requested
580// TRIALS 3 independent random graphs drawn from the same RNG stream
581// Edge model random_graph_edges(): Bernoulli p=0.36, weight = base ∈ [1,40]
582// + 101*edge_index (unique, non-negative)
583// Budget 1000 ms per algorithm invocation (wall-clock via steady_clock)
584//
585// When a machine-specific baseline is established, record it here and tighten
586// the budget accordingly.
587// ============================================================================
588
590{
591 const char * env = std::getenv("PERF_TESTS_ENABLED");
592 if (env == nullptr or std::string(env) != "1")
593 GTEST_SKIP() << "Skipped: set PERF_TESTS_ENABLED=1 to run";
594
595 constexpr size_t N = 40;
596 constexpr size_t K = 25;
597 constexpr int TRIALS = 3;
598 constexpr long long BUDGET_MS = 1000; // milliseconds per algorithm call
599 constexpr uint64_t MASTER_SEED = 0xDEADBEEFCAFEBABEULL;
600
601 std::mt19937_64 rng(MASTER_SEED);
602
603 for (int trial = 0; trial < TRIALS; ++trial)
604 {
606 Node * src = built.nodes[0];
607 Node * tgt = built.nodes[N - 1];
608
609 // --- Yen k-shortest (simple) paths ---
610 {
611 const auto t0 = std::chrono::steady_clock::now();
612 const auto result = yen_k_shortest_paths<Graph>(built.g, src, tgt, K);
613 const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
614 std::chrono::steady_clock::now() - t0).count();
615
616 EXPECT_LE(elapsed, BUDGET_MS)
617 << "yen_k_shortest_paths exceeded budget on trial " << trial
618 << ": " << elapsed << " ms (budget " << BUDGET_MS << " ms,"
619 << " N=" << N << ", K=" << K << ", seed=" << MASTER_SEED << ")";
620
621 EXPECT_LE(result.size(), K) << "trial=" << trial;
622
623 // Non-decreasing cost sanity check
624 long long prev = std::numeric_limits<long long>::min();
625 for (auto it = result.get_it(); it.has_curr(); it.next_ne())
626 {
627 EXPECT_GE(it.get_curr().total_cost, prev) << "trial=" << trial;
628 prev = it.get_curr().total_cost;
629 }
630 }
631
632 // --- Eppstein k-shortest (possibly non-simple) paths ---
633 {
634 const auto t0 = std::chrono::steady_clock::now();
635 const auto result = eppstein_k_shortest_paths<Graph>(built.g, src, tgt, K);
636 const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
637 std::chrono::steady_clock::now() - t0).count();
638
639 EXPECT_LE(elapsed, BUDGET_MS)
640 << "eppstein_k_shortest_paths exceeded budget on trial " << trial
641 << ": " << elapsed << " ms (budget " << BUDGET_MS << " ms,"
642 << " N=" << N << ", K=" << K << ", seed=" << MASTER_SEED << ")";
643
644 EXPECT_LE(result.size(), K) << "trial=" << trial;
645
646 long long prev = std::numeric_limits<long long>::min();
647 for (auto it = result.get_it(); it.has_curr(); it.next_ne())
648 {
649 EXPECT_GE(it.get_curr().total_cost, prev) << "trial=" << trial;
650 prev = it.get_curr().total_cost;
651 }
652 }
653 }
654}
K-shortest path algorithms (Yen and Eppstein-style API).
WeightedDigraph::Node Node
WeightedDigraph::Arc Arc
long double w
Definition btreepic.C:153
bool has_curr() const noexcept
Check if there is a current valid item.
Definition array_it.H:219
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:139
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
Definition tpl_array.H:351
T & append(const T &data)
Append a copy of data
Definition tpl_array.H:245
void reserve(size_t cap)
Reserves cap cells into the array.
Definition tpl_array.H:315
Generic directed graph (digraph) wrapper template.
Definition graph-dry.H:3854
size_t size() const noexcept
Return the current dimension of array.
T pop()
Remove the last item of array (as if this was a stack)
T & append()
Allocate a new entry to the end of array.
void next_ne() noexcept
Advances the iterator to the next filtered element (noexcept version).
Node Node
The graph type.
Definition tpl_graph.H:432
Arc Arc
The node class type.
Definition tpl_graph.H:433
Open addressing hash table with linear probing collision resolution.
Definition tpl_olhash.H:170
constexpr bool contains(const Key &key) const noexcept
Alias for has().
Definition hashDry.H:425
void remove(const Key &key)
Remove the key referenced by key.
Definition tpl_olhash.H:529
Iterator on nodes and arcs of a path.
Definition tpl_graph.H:3208
bool has_current_node() const noexcept
Return true if the iterator has a current node.
Definition tpl_graph.H:3321
Path on a graph.
Definition tpl_graph.H:2669
Key * insert(const Key &key)
Inserts a key into the hash table (copy version).
Definition hashDry.H:203
#define TEST(name)
static mt19937 rng
#define N
Definition fib.C:294
DynArray< Graph::Node * > nodes
Definition graphpic.C:406
DFSResult< typename Domain::Distance > dfs(Domain &domain, typename Domain::State &state, SearchPath< typename Domain::Move > &path, const typename Domain::Distance g, const typename Domain::Distance threshold, const size_t depth, IDAStarResult< Solution, typename Domain::Distance > &result, OnSolution &on_solution)
Core recursive DFS used by IDA* for a single threshold pass.
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
and
Check uniqueness with explicit hash + equality functors.
bool all(Container &container, Operation &operation)
Return true if all elements satisfy a predicate.
bool operator==(const DynList< T > &l1, const DynList< T > &l2)
Equality operator for DynList.
Divide_Conquer_DP_Result< Cost > divide_and_conquer_partition_dp(const size_t groups, const size_t n, Transition_Cost_Fn transition_cost, const Cost inf=dp_optimization_detail::default_inf< Cost >())
Optimize partition DP using divide-and-conquer optimization.
static std::atomic< bool > init
Definition hash-fct.C:53
Iterator on the items of an array.
Definition tpl_array.H:470
Arc of graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:222
Filtered iterator of adjacent arcs of a node.
Definition tpl_graph.H:1119
static int * k
Dynamic array container with automatic resizing.
Lazy and scalable dynamic array implementation.
Generic graph and digraph implementations.
Open addressing hash table with linear probing.