Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
johnson_test.cc
Go to the documentation of this file.
1
2/*
3 Aleph_w
4
5 Data structures & Algorithms
6 version 2.0.0b
7 https://github.com/lrleon/Aleph-w
8
9 This file is part of Aleph-w library
10
11 Copyright (c) 2002-2026 Leandro Rabindranath Leon
12
13 Permission is hereby granted, free of charge, to any person obtaining a copy
14 of this software and associated documentation files (the "Software"), to deal
15 in the Software without restriction, including without limitation the rights
16 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 copies of the Software, and to permit persons to whom the Software is
18 furnished to do so, subject to the following conditions:
19
20 The above copyright notice and this permission notice shall be included in all
21 copies or substantial portions of the Software.
22
23 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 SOFTWARE.
30*/
31
32
53#include <gtest/gtest.h>
54#include <tpl_graph.H>
55#include <Johnson.H>
56#include <Dijkstra.H>
57#include <Bellman_Ford.H>
58#include <tpl_graph_utils.H>
59#include <random>
60#include <limits>
61#include <vector>
62#include <map>
63#include <chrono>
64#include <cmath>
65
66using namespace Aleph;
67
68// ==================== Type definitions ====================
69
70// Node and Arc types for weighted digraphs
71struct WeightedNode : public Graph_Node<int>
72{
73 using Graph_Node<int>::Graph_Node;
74};
75
76struct WeightedArc : public Graph_Arc<double>
77{
78 using Graph_Arc<double>::Graph_Arc;
79};
80
84
85// Custom distance accessor for double weights
86struct DoubleDistance
87{
89
91 {
92 return arc->get_info();
93 }
94
95 static void set_zero(Arc* arc)
96 {
97 arc->get_info() = 0.0;
98 }
99
101 {
102 arc->get_info() = w;
103 }
104};
105
106// ==================== Test Fixtures ====================
107
108class JohnsonTest : public ::testing::Test
109{
110protected:
112 std::vector<Node*> nodes;
113
114 void SetUp() override
115 {
116 // Clean state for each test
117 nodes.clear();
118 }
119
120 void TearDown() override
121 {
122 // Graph destructor handles cleanup
123 }
124
125 // Helper to create a path graph: 0 -> 1 -> 2 -> ... -> n-1
126 void createPathGraph(int n, double weight = 1.0)
127 {
128 nodes.reserve(n);
129 for (int i = 0; i < n; ++i)
130 nodes.push_back(g.insert_node(i));
131
132 for (int i = 0; i < n - 1; ++i)
133 g.insert_arc(nodes[i], nodes[i + 1], weight);
134 }
135
136 // Helper to create a complete graph with random weights
137 void createCompleteGraph(int n, double minWeight = 1.0, double maxWeight = 10.0,
138 unsigned seed = 42)
139 {
140 std::mt19937 gen(seed);
141 std::uniform_real_distribution<double> dist(minWeight, maxWeight);
142
143 nodes.reserve(n);
144 for (int i = 0; i < n; ++i)
145 nodes.push_back(g.insert_node(i));
146
147 for (int i = 0; i < n; ++i)
148 for (int j = 0; j < n; ++j)
149 if (i != j)
150 g.insert_arc(nodes[i], nodes[j], dist(gen));
151 }
152
153 // Helper to create a graph with negative edges (but no negative cycles)
155 {
156 // Classic example graph with negative edge (3 -> 1 with weight -3):
157 // 0 --2--> 1 --1--> 2
158 // | ^
159 // 1 |
160 // v |
161 // 3 ----(-3)---> 1
162 // 3 -----(2)---> 2
163
164 nodes.push_back(g.insert_node(0)); // node 0
165 nodes.push_back(g.insert_node(1)); // node 1
166 nodes.push_back(g.insert_node(2)); // node 2
167 nodes.push_back(g.insert_node(3)); // node 3
168
169 g.insert_arc(nodes[0], nodes[1], 2.0); // 0 -> 1, weight 2
170 g.insert_arc(nodes[1], nodes[2], 1.0); // 1 -> 2, weight 1
171 g.insert_arc(nodes[0], nodes[3], 1.0); // 0 -> 3, weight 1
172 g.insert_arc(nodes[3], nodes[1], -3.0); // 3 -> 1, weight -3
173 g.insert_arc(nodes[3], nodes[2], 2.0); // 3 -> 2, weight 2
174 }
175
176 // Helper to create a graph with a negative cycle
178 {
179 nodes.push_back(g.insert_node(0));
180 nodes.push_back(g.insert_node(1));
181 nodes.push_back(g.insert_node(2));
182
183 g.insert_arc(nodes[0], nodes[1], 1.0); // 0 -> 1
184 g.insert_arc(nodes[1], nodes[2], -2.0); // 1 -> 2
185 g.insert_arc(nodes[2], nodes[0], -1.0); // 2 -> 0 (creates cycle with total -2)
186 }
187
188 // Helper to create a sparse random graph
189 void createSparseGraph(int n, double edgeProbability = 0.2,
190 double minWeight = 1.0, double maxWeight = 10.0,
191 unsigned seed = 42)
192 {
193 std::mt19937 gen(seed);
194 std::uniform_real_distribution<double> weightDist(minWeight, maxWeight);
195 std::uniform_real_distribution<double> probDist(0.0, 1.0);
196
197 nodes.reserve(n);
198 for (int i = 0; i < n; ++i)
199 nodes.push_back(g.insert_node(i));
200
201 for (int i = 0; i < n; ++i)
202 for (int j = 0; j < n; ++j)
203 if (i != j && probDist(gen) < edgeProbability)
204 g.insert_arc(nodes[i], nodes[j], weightDist(gen));
205 }
206
207 // Compute all-pairs shortest paths using Floyd-Warshall for verification
208 std::map<std::pair<int, int>, double> computeFloydWarshall()
209 {
210 const double INF = std::numeric_limits<double>::infinity();
211 int n = nodes.size();
212
213 // Initialize distance matrix
214 std::vector<std::vector<double>> dist(n, std::vector<double>(n, INF));
215
216 for (int i = 0; i < n; ++i)
217 dist[i][i] = 0.0;
218
219 // Fill in edge weights
220 for (Arc_Iterator<TestDigraph> it(g); it.has_curr(); it.next())
221 {
222 auto arc = it.get_curr();
223 auto src = g.get_src_node(arc);
224 auto tgt = g.get_tgt_node(arc);
225 int i = src->get_info();
226 int j = tgt->get_info();
227 double w = arc->get_info();
228 if (w < dist[i][j])
229 dist[i][j] = w;
230 }
231
232 // Floyd-Warshall relaxation
233 for (int k = 0; k < n; ++k)
234 for (int i = 0; i < n; ++i)
235 for (int j = 0; j < n; ++j)
236 if (dist[i][k] != INF && dist[k][j] != INF)
237 if (dist[i][k] + dist[k][j] < dist[i][j])
238 dist[i][j] = dist[i][k] + dist[k][j];
239
240 // Convert to map
241 std::map<std::pair<int, int>, double> result;
242 for (int i = 0; i < n; ++i)
243 for (int j = 0; j < n; ++j)
244 if (dist[i][j] != INF)
245 result[{i, j}] = dist[i][j];
246
247 return result;
248 }
249};
250
251// ==================== Basic Bellman-Ford Tests (Prerequisites) ====================
252
254{
255 // Simple path graph: 0 -> 1 -> 2
256 createPathGraph(3, 2.0);
257
259 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
260
262 EXPECT_TRUE(bf.is_painted());
263}
264
266{
267 createNegativeWeightGraph();
268
270 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
271
272 EXPECT_FALSE(hasNegCycle) << "Graph has negative weights but no negative cycles";
273}
274
276{
277 createNegativeCycleGraph();
278
280 bool hasNegCycle = bf.has_negative_cycle(nodes[0]);
281
282 EXPECT_TRUE(hasNegCycle) << "Graph should have a negative cycle";
283}
284
286{
287 createNegativeWeightGraph();
288
290
291 // This should not throw (no negative cycles)
292 auto weights = bf.compute_nodes_weights();
293
294 // All original nodes should have weights
295 EXPECT_EQ(weights.size(), nodes.size());
296
297 // Verify weights are finite
298 for (auto node : nodes)
299 {
300 auto* weightPtr = weights.search(node);
301 ASSERT_NE(weightPtr, nullptr) << "Node should have a weight";
302 EXPECT_TRUE(std::isfinite(weightPtr->second));
303 }
304}
305
307{
308 createNegativeCycleGraph();
309
311
312 // This should throw because of negative cycle
313 EXPECT_THROW(bf.compute_nodes_weights(), std::domain_error);
314}
315
316// ==================== Johnson Algorithm Tests ====================
317
319{
320 createPathGraph(4, 1.0);
321
323 EXPECT_TRUE(johnson.is_initialized());
324
325 // Distance from 0 to 3 should be 3.0 (three edges of weight 1.0)
326 double dist = johnson.get_distance(nodes[0], nodes[3]);
327 EXPECT_DOUBLE_EQ(dist, 3.0);
328
329 // Distance from 0 to 1 should be 1.0
330 dist = johnson.get_distance(nodes[0], nodes[1]);
331 EXPECT_DOUBLE_EQ(dist, 1.0);
332
333 // Distance from 0 to 0 should be 0.0
334 dist = johnson.get_distance(nodes[0], nodes[0]);
335 EXPECT_DOUBLE_EQ(dist, 0.0);
336}
337
339{
340 createNegativeWeightGraph();
341 // Graph structure:
342 // 0 --2--> 1 --1--> 2
343 // |
344 // 1
345 // v
346 // 3 --(-3)--> 1
347 // 3 ---(2)--> 2
348 //
349 // Expected distances from 0:
350 // 0->0: 0, 0->1: -2 (via 3), 0->2: -1 (via 3->1), 0->3: 1
351 // Expected distances from 1:
352 // 1->1: 0, 1->2: 1
353 // Expected distances from 3:
354 // 3->1: -3, 3->2: -2 (via 1), 3->3: 0
355
357 EXPECT_TRUE(johnson.is_initialized());
358
359 // Test known reachable pairs with expected distances
360 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[0], nodes[0]), 0.0);
361 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[0], nodes[1]), -2.0); // via 0->3->1
362 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[0], nodes[2]), -1.0); // via 0->3->1->2
363 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[0], nodes[3]), 1.0);
364
365 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[1], nodes[1]), 0.0);
366 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[1], nodes[2]), 1.0);
367
368 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[2], nodes[2]), 0.0);
369
370 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[3], nodes[1]), -3.0);
371 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[3], nodes[2]), -2.0); // via 3->1->2
372 EXPECT_DOUBLE_EQ(johnson.get_distance(nodes[3], nodes[3]), 0.0);
373}
374
376{
377 createNegativeCycleGraph();
378
379 // Johnson should throw on negative cycle detection
381 EXPECT_THROW(JohnsonType johnson(g), std::domain_error);
382}
383
385{
386 createNegativeWeightGraph();
387
389 auto allPairs = johnson.compute_all_pairs_distances();
390
391 // Verify all pairs distances match expected values
392 // Based on the graph structure, we know exactly which pairs are reachable
393
394 // Helper to check finite distance
395 auto checkFiniteDist = [&](int i, int j, double expected) {
396 auto* entry = allPairs.search(std::make_pair(nodes[i], nodes[j]));
397 ASSERT_NE(entry, nullptr) << "Missing entry for (" << i << ", " << j << ")";
398 EXPECT_NEAR(entry->second, expected, 1e-9)
399 << "Distance mismatch for (" << i << ", " << j << ")";
400 };
401
402 // Only verify reachable pairs (finite distances)
403 // Unreachable pairs may or may not be in allPairs
404
405 // From node 0: can reach all
406 checkFiniteDist(0, 0, 0.0);
407 checkFiniteDist(0, 1, -2.0); // via 0->3->1
408 checkFiniteDist(0, 2, -1.0); // via 0->3->1->2
409 checkFiniteDist(0, 3, 1.0);
410
411 // From node 1: can reach 1 and 2
412 checkFiniteDist(1, 1, 0.0);
413 checkFiniteDist(1, 2, 1.0);
414
415 // From node 2: can only reach itself
416 checkFiniteDist(2, 2, 0.0);
417
418 // From node 3: can reach 1, 2, and itself
419 checkFiniteDist(3, 1, -3.0);
420 checkFiniteDist(3, 2, -2.0); // via 3->1->2
421 checkFiniteDist(3, 3, 0.0);
422}
423
424// ==================== Reweighting Verification Tests ====================
425
427{
428 // Test that Johnson's reweighting preserves shortest path structure
429 // After reweighting: w'(u,v) = w(u,v) + h(u) - h(v)
430 // For any path p from s to t: w'(p) = w(p) + h(s) - h(t)
431 // So shortest paths are preserved since h(s) - h(t) is constant
432
433 createNegativeWeightGraph();
434
436 auto weights = bf.compute_nodes_weights();
437
438 // Verify the reweighting formula produces non-negative weights
439 for (Arc_Iterator<TestDigraph> it(g); it.has_curr(); it.next())
440 {
441 auto arc = it.get_curr();
442 auto u = g.get_src_node(arc);
443 auto v = g.get_tgt_node(arc);
444 double w = arc->get_info();
445
446 auto* hu_ptr = weights.search(u);
447 auto* hv_ptr = weights.search(v);
448 ASSERT_NE(hu_ptr, nullptr);
449 ASSERT_NE(hv_ptr, nullptr);
450
451 double hu = hu_ptr->second;
452 double hv = hv_ptr->second;
453 double w_prime = w + hu - hv;
454
455 EXPECT_GE(w_prime, 0.0)
456 << "Reweighted edge should be non-negative: w=" << w
457 << ", h(u)=" << hu << ", h(v)=" << hv
458 << ", w'=" << w_prime;
459 }
460}
461
462// ==================== Dijkstra on Reweighted Graph Tests ====================
463
465{
466 createNegativeWeightGraph();
467
468 // Get node weights from Bellman-Ford
470 auto weights = bf.compute_nodes_weights();
471
472 // Create a copy and reweight the edges
475
476 // Copy nodes
477 for (auto it = g.get_node_it(); it.has_curr(); it.next())
478 {
479 auto orig = it.get_curr();
480 auto copy = reweighted.insert_node(orig->get_info());
482 }
483
484 // Copy and reweight arcs
485 for (Arc_Iterator<TestDigraph> it(g); it.has_curr(); it.next())
486 {
487 auto arc = it.get_curr();
488 auto u = g.get_src_node(arc);
489 auto v = g.get_tgt_node(arc);
490 double w = arc->get_info();
491
492 double hu = weights.find(u);
493 double hv = weights.find(v);
494 double w_prime = w + hu - hv;
495
496 auto* uCopy = nodeMap.search(u);
497 auto* vCopy = nodeMap.search(v);
498 reweighted.insert_arc(uCopy->second, vCopy->second, w_prime);
499 }
500
501 // Now Dijkstra should work on the reweighted graph
502 // (all weights are non-negative)
503 auto* srcPtr = nodeMap.search(nodes[0]);
504 ASSERT_NE(srcPtr, nullptr);
506 dijkstra.paint_min_paths_tree(reweighted, srcPtr->second);
507
508 EXPECT_TRUE(dijkstra.is_painted());
509}
510
511// ==================== Comprehensive All-Pairs Tests ====================
512
514{
515 // Small complete graph to verify all-pairs computation
516 createCompleteGraph(5, 1.0, 10.0);
517
518 auto floydResults = computeFloydWarshall();
519
520 // Verify Floyd-Warshall gives us some results
522
523 // All pairs should be reachable in a complete graph
524 int n = nodes.size();
525 for (int i = 0; i < n; ++i)
526 for (int j = 0; j < n; ++j)
527 EXPECT_NE(floydResults.find({i, j}), floydResults.end())
528 << "Path from " << i << " to " << j << " should exist";
529}
530
532{
533 // Sparse graph - some pairs may be unreachable
534 createSparseGraph(10, 0.3, 1.0, 10.0);
535
536 auto floydResults = computeFloydWarshall();
537
538 // At least self-loops should have distance 0
539 for (int i = 0; i < (int)nodes.size(); ++i)
540 {
541 auto it = floydResults.find({i, i});
543 EXPECT_DOUBLE_EQ(it->second, 0.0);
544 }
545}
546
547// ==================== Edge Cases ====================
548
550{
551 nodes.push_back(g.insert_node(0));
552
554 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
555
557}
558
560{
561 nodes.push_back(g.insert_node(0));
562 nodes.push_back(g.insert_node(1));
563
565 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
566
568}
569
571{
572 nodes.push_back(g.insert_node(0));
573 nodes.push_back(g.insert_node(1));
574 g.insert_arc(nodes[0], nodes[1], 5.0);
575
577 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
578
580 EXPECT_TRUE(bf.is_painted());
581}
582
584{
585 nodes.push_back(g.insert_node(0));
586 g.insert_arc(nodes[0], nodes[0], 1.0);
587
589 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
590
592}
593
595{
596 nodes.push_back(g.insert_node(0));
597 g.insert_arc(nodes[0], nodes[0], -1.0); // Negative self-loop = negative cycle
598
600 bool hasNegCycle = bf.has_negative_cycle(nodes[0]);
601
603}
604
606{
607 nodes.push_back(g.insert_node(0));
608 nodes.push_back(g.insert_node(1));
609 g.insert_arc(nodes[0], nodes[1], 5.0);
610 g.insert_arc(nodes[0], nodes[1], 3.0); // Shorter parallel edge
611 g.insert_arc(nodes[0], nodes[1], 7.0); // Longer parallel edge
612
614 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
615
617}
618
619// ==================== Stress Tests ====================
620
622{
623 createCompleteGraph(20, 1.0, 100.0);
624
626
627 // Bellman-Ford from node 0
628 bool hasNegCycle = bf.paint_spanning_tree(nodes[0]);
630
631 // Verify we can get paths to all nodes
632 for (size_t i = 1; i < nodes.size(); ++i)
633 {
634 Path<TestDigraph> path(g);
635 bf.get_min_path(nodes[i], path);
636 EXPECT_FALSE(path.is_empty()) << "Path to node " << i << " should exist";
637 }
638}
639
641{
642 // Disabled by default - for manual performance testing
643 createCompleteGraph(100, 1.0, 100.0);
644
645 // Johnson should be O(V^2 log V + VE) which is better than
646 // Floyd-Warshall O(V^3) for sparse graphs
647
648 auto start = std::chrono::high_resolution_clock::now();
649
650 // Bellman-Ford for node weights
652 auto weights = bf.compute_nodes_weights();
653
654 auto mid = std::chrono::high_resolution_clock::now();
655
656 // Then |V| Dijkstra executions
657 // (simplified - full Johnson would reweight and run Dijkstra from each node)
658
659 auto end = std::chrono::high_resolution_clock::now();
660
661 auto bfTime = std::chrono::duration_cast<std::chrono::milliseconds>(mid - start);
662 auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
663
664 std::cout << "Bellman-Ford time: " << bfTime.count() << " ms\n";
665 std::cout << "Total time: " << totalTime.count() << " ms\n";
666}
667
668// ==================== Correctness vs Floyd-Warshall ====================
669
671{
672 // Create a small graph with mixed weights (some negative)
673 nodes.push_back(g.insert_node(0));
674 nodes.push_back(g.insert_node(1));
675 nodes.push_back(g.insert_node(2));
676 nodes.push_back(g.insert_node(3));
677
678 g.insert_arc(nodes[0], nodes[1], 3.0);
679 g.insert_arc(nodes[0], nodes[2], 8.0);
680 g.insert_arc(nodes[1], nodes[2], -2.0); // Negative edge
681 g.insert_arc(nodes[1], nodes[3], 1.0);
682 g.insert_arc(nodes[2], nodes[3], 2.0);
683
684 auto floydResults = computeFloydWarshall();
685
686 // Expected distances from node 0:
687 // 0->0: 0
688 // 0->1: 3
689 // 0->2: 3 + (-2) = 1 (via 0->1->2)
690 // 0->3: 3 + (-2) + 2 = 3 (via 0->1->2->3) or 3 + 1 = 4 (via 0->1->3)
691 // min = 3
692
693 auto d00 = floydResults[std::make_pair(0, 0)];
694 auto d01 = floydResults[std::make_pair(0, 1)];
695 auto d02 = floydResults[std::make_pair(0, 2)];
696 auto d03 = floydResults[std::make_pair(0, 3)];
697
698 EXPECT_DOUBLE_EQ(d00, 0.0);
699 EXPECT_DOUBLE_EQ(d01, 3.0);
700 EXPECT_DOUBLE_EQ(d02, 1.0);
701 EXPECT_DOUBLE_EQ(d03, 3.0);
702
703 // Now verify with Bellman-Ford (as a step towards Johnson)
705 bf.paint_spanning_tree(nodes[0]);
706
707 // Get distances from Bellman-Ford
708 Path<TestDigraph> path(g);
709
710 // Distance to node 2
711 auto dist2 = bf.get_min_path(nodes[2], path);
712 EXPECT_DOUBLE_EQ(dist2, 1.0) << "Distance 0->2 should be 1.0";
713
714 // Distance to node 3
715 path = Path<TestDigraph>(g);
716 auto dist3 = bf.get_min_path(nodes[3], path);
717 EXPECT_DOUBLE_EQ(dist3, 3.0) << "Distance 0->3 should be 3.0";
718}
719
720// ==================== SPFA Variant Tests ====================
721
723{
724 createNegativeWeightGraph();
725
726 // Standard Bellman-Ford
728 bool hasNeg1 = bf1.paint_spanning_tree(nodes[0]);
729
730 // SPFA variant (faster version)
732 bool hasNeg2 = bf2.faster_paint_spanning_tree(nodes[0]);
733
735
736 // Both should detect no negative cycle
739}
740
741// ==================== Negative Cycle Tests ====================
742
744{
745 createNegativeCycleGraph();
746
748 auto cycle = bf.test_negative_cycle(nodes[0]);
749
750 // If negative cycle is found, path should not be empty
751 // and should form a cycle
752 if (!cycle.is_empty())
753 {
754 // In Aleph-w, the path might not explicitly repeat the first node
755 // but should form a connected cycle
756 EXPECT_GE(cycle.size(), 2) << "Cycle should have at least 2 nodes";
757 }
758}
759
761{
762 // Create a graph where negative cycle exists but is not reachable from start
763 nodes.push_back(g.insert_node(0)); // Isolated node
764 nodes.push_back(g.insert_node(1)); // Part of cycle
765 nodes.push_back(g.insert_node(2)); // Part of cycle
766
767 // Cycle between nodes 1 and 2 (not reachable from 0)
768 g.insert_arc(nodes[1], nodes[2], -1.0);
769 g.insert_arc(nodes[2], nodes[1], -1.0);
770
772
773 // From node 0, should not find the negative cycle
774 bool hasNegFromNode0 = bf.has_negative_cycle(nodes[0]);
775 EXPECT_FALSE(hasNegFromNode0) << "Negative cycle not reachable from node 0";
776
777 // Global check should find it
779 bool hasNegGlobal = bf2.has_negative_cycle();
780 EXPECT_TRUE(hasNegGlobal) << "Global check should find unreachable negative cycle";
781}
782
783// ==================== Integration Test: Manual Johnson Implementation ====================
784
786{
787 // This test manually implements Johnson's algorithm to verify correctness
788 // and serves as a reference for fixing the actual implementation
789
790 createNegativeWeightGraph();
791
792 // Step 1: Add dummy node connected to all with 0-weight edges
793 // and run Bellman-Ford to get node potentials
796
797 try
798 {
799 h = bf.compute_nodes_weights();
800 }
801 catch (const std::domain_error&)
802 {
803 FAIL() << "Unexpected negative cycle in test graph";
804 }
805
806 // Step 2: Reweight edges: w'(u,v) = w(u,v) + h(u) - h(v)
807 std::map<Arc*, double> originalWeights;
808 std::map<Arc*, double> reweightedWeights;
809
810 for (Arc_Iterator<TestDigraph> it(g); it.has_curr(); it.next())
811 {
812 auto arc = it.get_curr();
813 auto u = g.get_src_node(arc);
814 auto v = g.get_tgt_node(arc);
815 double w = arc->get_info();
816
817 originalWeights[arc] = w;
818
819 double hu = h.find(u);
820 double hv = h.find(v);
821 double w_prime = w + hu - hv;
822
823 EXPECT_GE(w_prime, -1e-9) << "Reweighted edge should be non-negative";
824
826 arc->get_info() = w_prime; // Apply reweighting
827 }
828
829 // Step 3: Run Dijkstra from each source
830 std::map<std::pair<int, int>, double> johnsonDistances;
831
832 for (size_t i = 0; i < nodes.size(); ++i)
833 {
835 dijkstra.paint_min_paths_tree(g, nodes[i]);
836
837 for (size_t j = 0; j < nodes.size(); ++j)
838 {
839 if (i == j)
840 {
841 johnsonDistances[{(int)i, (int)j}] = 0.0;
842 continue;
843 }
844
845 Path<TestDigraph> path(g);
846 try
847 {
848 double d_prime = dijkstra.get_min_path(nodes[j], path);
849
850 // Step 4: Adjust back: d(u,v) = d'(u,v) - h(u) + h(v)
851 double hu = h.find(nodes[i]);
852 double hv = h.find(nodes[j]);
853 double d = d_prime - hu + hv;
854
855 johnsonDistances[{(int)i, (int)j}] = d;
856 }
857 catch (...)
858 {
859 // Node not reachable
860 }
861 }
862 }
863
864 // Restore original weights
865 for (Arc_Iterator<TestDigraph> it(g); it.has_curr(); it.next())
866 {
867 auto arc = it.get_curr();
868 arc->get_info() = originalWeights[arc];
869 }
870
871 // Compare with Floyd-Warshall
872 auto floydDistances = computeFloydWarshall();
873
874 for (const auto& [pair, dist] : johnsonDistances)
875 {
876 auto it = floydDistances.find(pair);
877 if (it != floydDistances.end())
878 {
879 EXPECT_NEAR(dist, it->second, 1e-9)
880 << "Distance mismatch for pair (" << pair.first << ", " << pair.second << "): "
881 << "Johnson=" << dist << ", Floyd=" << it->second;
882 }
883 }
884}
885
886// ==================== Main ====================
887
888int main(int argc, char **argv)
889{
890 ::testing::InitGoogleTest(&argc, argv);
891 return RUN_ALL_TESTS();
892}
Bellman-Ford algorithm for single-source shortest paths.
Dijkstra's shortest path algorithm.
Johnson's algorithm for all-pairs shortest paths.
int main()
WeightedDigraph::Node Node
WeightedDigraph::Arc Arc
long double h
Definition btreepic.C:154
long double w
Definition btreepic.C:153
Bellman-Ford algorithm for shortest paths with negative weights.
Generic directed graph (digraph) wrapper template.
Definition graph-dry.H:3848
typename BaseGraph::Arc Arc
Definition graph-dry.H:3852
typename BaseGraph::Node Node
Definition graph-dry.H:3851
Spanning tree calculation of all shortest paths from a given node according to Dijkstra's algorithm.
Definition Dijkstra.H:101
T & insert(const T &item)
Insert a new item by copy.
Definition htlist.H:1502
void empty() noexcept
empty the list
Definition htlist.H:1689
Generic key-value map implemented on top of a binary search tree.
void next()
Advances the iterator to the next filtered element.
constexpr bool is_empty() const noexcept
Return true if list is empty.
Definition htlist.H:523
size_t size() const noexcept
Count the number of elements of the list.
Definition htlist.H:1319
Johnson's algorithm for all-pairs shortest paths.
Definition Johnson.H:125
Path on a graph.
Definition tpl_graph.H:2669
bool is_empty() const noexcept
Return true if the path is empty.
Definition tpl_graph.H:2813
TestDigraph g
void createSparseGraph(int n, double edgeProbability=0.2, double minWeight=1.0, double maxWeight=10.0, unsigned seed=42)
void TearDown() override
std::map< std::pair< int, int >, double > computeFloydWarshall()
std::vector< Node * > nodes
void createPathGraph(int n, double weight=1.0)
void createNegativeWeightGraph()
void createNegativeCycleGraph()
void SetUp() override
void createCompleteGraph(int n, double minWeight=1.0, double maxWeight=10.0, unsigned seed=42)
iterator end() noexcept
Return an STL-compatible end iterator.
#define FAIL(msg)
DynArray< Graph::Node * > nodes
Definition graphpic.C:406
TEST_F(JohnsonTest, BellmanFordBasic)
TestDigraph::Arc Arc
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
Itor2 copy(Itor1 sourceBeg, const Itor1 &sourceEnd, Itor2 destBeg)
Copy elements from one range to another.
Definition ahAlgo.H:584
std::pair< First, Second > pair
Alias to std::pair kept for backwards compatibility.
Definition ahPair.H:89
DynList< T > maps(const C &c, Op op)
Classic map operation.
Filtered iterator on all the arcs of a graph.
Definition tpl_graph.H:1164
Arc of graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:222
Node belonging to a graph implemented with a double linked adjacency list.
Definition tpl_graph.H:121
Distance_Type operator()(Arc *arc) const
static void set_zero(Arc *arc)
double Distance_Type
void set_weight(Arc *arc, Distance_Type w)
Generic graph and digraph implementations.
Utility algorithms and operations for graphs.