Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
blossom_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
46# include <gtest/gtest.h>
47
48# include <algorithm>
49# include <bit>
50# include <chrono>
51# include <cstdint>
52# include <cstdlib>
53# include <functional>
54# include <random>
55# include <string>
56# include <stdexcept>
57# include <utility>
58# include <vector>
59
60# include <Blossom.H>
61# include <tpl_array.H>
62# include <tpl_bipartite.H>
63# include <tpl_dynArray.H>
64# include <tpl_dynSetTree.H>
65# include <tpl_graph.H>
66# include <tpl_olhash.H>
67# include <tpl_sgraph.H>
68# include <tpl_agraph.H>
69
70using namespace Aleph;
71
72namespace
73{
74 template <class GT>
75 GT build_graph(size_t n,
76 const DynArray<std::pair<size_t, size_t>> & edges,
77 int arc_info = 1)
78 {
79 GT g;
80 for (size_t i = 0; i < n; ++i)
81 static_cast<void>(g.insert_node(static_cast<int>(i)));
82
85 [&](typename GT::Node * p)
86 {
87 const size_t id = static_cast<size_t>(p->get_info());
88 if (id < n)
89 nodes(id) = p;
90 });
91
92 for (size_t i = 0; i < n; ++i)
93 ah_runtime_error_if(nodes(i) == nullptr)
94 << "build_graph(): node id " << i << " not found after insertion";
95
96 for (const auto & [u, v] : edges)
97 {
98 if (u >= n or v >= n)
99 continue;
100 g.insert_arc(nodes(u), nodes(v), arc_info);
101 }
102
103 return g;
104 }
105
106 template <class GT>
107 GT build_graph(size_t n,
108 std::initializer_list<std::pair<size_t, size_t>> edges,
109 int arc_info = 1)
110 {
112 aleph_edges.reserve(edges.size());
113 for (const auto & edge : edges)
114 aleph_edges.append(edge);
115
116 return build_graph<GT>(n, aleph_edges, arc_info);
117 }
118
119
120
121 template <class GT>
123 {
125 for (typename GT::Arc_Iterator it(g); it.has_curr(); it.next_ne())
126 arcs_in_graph.insert(it.get_curr());
127
129 for (auto it = matching.get_it(); it.has_curr(); it.next_ne())
130 {
131 auto * arc = it.get_curr();
132 if (arc == nullptr)
133 return false;
134
135 if (not arcs_in_graph.contains(arc))
136 return false;
137
138 auto * src = g.get_src_node(arc);
139 auto * tgt = g.get_tgt_node(arc);
140 if (src == nullptr or tgt == nullptr)
141 return false;
142
143 if (src == tgt)
144 return false;
145
146 if (used_nodes.contains_or_insert(src).second)
147 return false;
148
149 if (used_nodes.contains_or_insert(tgt).second)
150 return false;
151 }
152
153 return true;
154 }
155
157 (size_t n, const DynArray<std::pair<size_t, size_t>> & edges)
158 {
160
161 for (size_t i = 0; i < edges.size(); ++i)
162 {
163 auto [u, v] = edges(i);
164 if (u >= n or v >= n or u == v)
165 continue;
166 if (u > v)
167 std::swap(u, v);
168 seen.insert(std::make_pair(u, v));
169 }
170
172 for (const auto & p : seen)
173 simple_edges.append(p);
174
175 return simple_edges;
176 }
177
178 size_t exact_maximum_matching_size(size_t n,
179 const DynArray<std::pair<size_t, size_t>> & edges)
180 {
181 if (n == 0)
182 return 0;
183
184 // Enough for all tests in this file; keeps DP vector small and fast.
185 ah_runtime_error_if(n > 20) << "exact_maximum_matching_size supports n <= 20";
186
187 Array<uint64_t> adj(n, uint64_t{0});
188 auto simple = normalize_simple_edges(n, edges);
189 for (size_t i = 0; i < simple.size(); ++i)
190 {
191 auto [u, v] = simple(i);
192 adj(u) |= (uint64_t{1} << v);
193 adj(v) |= (uint64_t{1} << u);
194 }
195
196 const size_t state_count = static_cast<size_t>(uint64_t{1} << n);
197 Array<int> memo(state_count, -1);
198
199 std::function<int(uint64_t)> solve = [&](uint64_t mask) -> int
200 {
201 if (mask == 0)
202 return 0;
203
204 int & ans = memo(static_cast<size_t>(mask));
205 if (ans != -1)
206 return ans;
207
208 const size_t i = static_cast<size_t>(std::countr_zero(mask));
209 const uint64_t rest = mask & ~(uint64_t{1} << i);
210
211 ans = solve(rest); // leave i unmatched
212
213 uint64_t options = adj(i) & rest;
214 while (options != 0)
215 {
216 const size_t j = static_cast<size_t>(std::countr_zero(options));
217 options &= (options - 1);
218 ans = std::max(ans,
219 1 + solve(rest & ~(uint64_t{1} << j)));
220 }
221
222 return ans;
223 };
224
225 const uint64_t all = (uint64_t{1} << n) - 1;
226 return static_cast<size_t>(solve(all));
227 }
228
229 template <class GT>
231 {
233 }
234
235 template <class GT>
236 GT build_bipartite_graph(size_t left_size,
237 size_t right_size,
238 const DynArray<std::pair<size_t, size_t>> & edges_lr)
239 {
240 GT g;
241 const size_t n = left_size + right_size;
242
243 for (size_t i = 0; i < left_size; ++i)
244 static_cast<void>(g.insert_node(static_cast<int>(i)));
245
246 for (size_t i = 0; i < right_size; ++i)
247 static_cast<void>(g.insert_node(static_cast<int>(left_size + i)));
248
251 [&](typename GT::Node * p)
252 {
253 const size_t id = static_cast<size_t>(p->get_info());
254 if (id < n)
255 nodes(id) = p;
256 });
257
258 for (size_t i = 0; i < n; ++i)
259 ah_runtime_error_if(nodes(i) == nullptr)
260 << "build_bipartite_graph(): node id " << i << " not found after insertion";
261
262 for (size_t i = 0; i < edges_lr.size(); ++i)
263 {
264 auto [l, r] = edges_lr(i);
265 if (l >= left_size or r >= right_size)
266 continue;
267 g.insert_arc(nodes(l), nodes(left_size + r), 1);
268 }
269
270 return g;
271 }
272
273 template <class GT>
274 class BlossomMatchingTypedTest : public ::testing::Test
275 {
276 };
277
278 using GraphBackends = ::testing::Types<
282
283 TYPED_TEST_SUITE(BlossomMatchingTypedTest, GraphBackends);
284
285} // namespace
286
287
288TYPED_TEST(BlossomMatchingTypedTest, EmptyGraph)
289{
290 using Graph = TypeParam;
291
292 Graph g;
294
295 const size_t cardinality = run_blossom(g, matching);
296
297 EXPECT_EQ(cardinality, 0u);
298 EXPECT_TRUE(matching.is_empty());
300}
301
302TYPED_TEST(BlossomMatchingTypedTest, SingleEdge)
303{
304 using Graph = TypeParam;
305
306 auto g = build_graph<Graph>(2, {{0, 1}});
308
309 const size_t cardinality = run_blossom(g, matching);
310
311 EXPECT_EQ(cardinality, 1u);
312 EXPECT_EQ(matching.size(), 1u);
314}
315
316TYPED_TEST(BlossomMatchingTypedTest, OddCycleC5)
317{
318 using Graph = TypeParam;
319
320 auto g = build_graph<Graph>(5, {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}});
322
323 const size_t cardinality = run_blossom(g, matching);
324
325 EXPECT_EQ(cardinality, 2u);
326 EXPECT_EQ(matching.size(), 2u);
328}
329
330TYPED_TEST(BlossomMatchingTypedTest, CompleteGraphK6)
331{
332 using Graph = TypeParam;
333
335 edges.reserve(15); // For K6, there are 6*5/2 = 15 edges
336 for (size_t i = 0; i < 6; ++i)
337 for (size_t j = i + 1; j < 6; ++j)
338 edges.append(std::make_pair(i, j));
339
340 auto g = build_graph<Graph>(6, edges);
342
343 const size_t cardinality = run_blossom(g, matching);
344
345 EXPECT_EQ(cardinality, 3u);
346 EXPECT_EQ(matching.size(), 3u);
348}
349
350TYPED_TEST(BlossomMatchingTypedTest, BlossomContractionScenario)
351{
352 using Graph = TypeParam;
353
354 // 5-cycle plus three stems:
355 // cycle: 0-1-2-3-4-0, stems: 1-5, 2-6, 3-7
356 // Maximum matching = 4
357 auto g = build_graph<Graph>(8,
358 {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0},
359 {1, 5}, {2, 6}, {3, 7}});
361
362 const size_t cardinality = run_blossom(g, matching);
363
364 EXPECT_EQ(cardinality, 4u);
365 EXPECT_EQ(matching.size(), 4u);
367}
368
369TYPED_TEST(BlossomMatchingTypedTest, HandlesLoopsAndParallelArcs)
370{
371 using Graph = TypeParam;
372
373 auto g = build_graph<Graph>(4,
374 {
375 {0, 0},
376 {0, 1},
377 {0, 1},
378 {1, 2},
379 {2, 3},
380 {3, 3}
381 });
382
384 const size_t cardinality = run_blossom(g, matching);
385
386 EXPECT_EQ(cardinality, 2u);
387 EXPECT_EQ(matching.size(), 2u);
389}
390
392{
393 using Graph = TypeParam;
394
395 auto g = build_graph<Graph>(7,
396 {{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4},
397 {4, 5}, {5, 6}, {1, 6}});
398
401
404
405 EXPECT_EQ(c1, c2);
409}
410
411TYPED_TEST(BlossomMatchingTypedTest, RandomGraphsAgreeWithExactDP)
412{
413 using Graph = TypeParam;
414
415 std::mt19937 rng(20260220);
416 std::uniform_real_distribution<double> coin(0.0, 1.0);
417
418 for (size_t n = 2; n <= 10; ++n)
419 for (int sample = 0; sample < 30; ++sample)
420 {
421 const double p = 0.10 + 0.03 * static_cast<double>(sample);
422
424 edges.reserve(n * (n - 1) / 2);
425 for (size_t u = 0; u < n; ++u)
426 for (size_t v = u + 1; v < n; ++v)
427 if (coin(rng) < p)
428 edges.append(std::make_pair(u, v));
429
430 auto g = build_graph<Graph>(n, edges);
431
433 const size_t blossom_size = run_blossom(g, matching);
434 const size_t exact_size = exact_maximum_matching_size(n, edges);
435
437 << "n=" << n << " sample=" << sample;
439 << "n=" << n << " sample=" << sample;
441 << "n=" << n << " sample=" << sample;
442 }
443}
444
446{
447 using Graph = TypeParam;
448
449 std::mt19937 rng(7);
450 std::uniform_real_distribution<double> coin(0.0, 1.0);
451
452 for (size_t left = 1; left <= 6; ++left)
453 for (size_t right = 1; right <= 6; ++right)
454 for (int sample = 0; sample < 10; ++sample)
455 {
456 const double p = 0.15 + 0.07 * static_cast<double>(sample);
458 edges_lr.reserve(left * right);
459
460 for (size_t l = 0; l < left; ++l)
461 for (size_t r = 0; r < right; ++r)
462 if (coin(rng) < p)
463 edges_lr.append(std::make_pair(l, r));
464
465 auto g = build_bipartite_graph<Graph>(left, right, edges_lr);
466
469
470 const size_t blossom_size =
473
475 << "left=" << left << " right=" << right << " sample=" << sample;
478 }
479}
480
481TYPED_TEST(BlossomMatchingTypedTest, NestedOddCyclesWithStems)
482{
483 using Graph = TypeParam;
484
485 // Two connected 5-cycles plus stems:
486 // C1: 0-1-2-3-4-0
487 // C2: 2-5-6-7-8-2
488 // stems: 1-9, 4-10, 6-11
489 //
490 // This family tends to trigger multiple blossom contractions and expansions.
492 {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0},
493 {2, 5}, {5, 6}, {6, 7}, {7, 8}, {8, 2},
494 {1, 9}, {4, 10}, {6, 11}
495 };
496
497 auto g = build_graph<Graph>(12, edges);
499
500 const size_t blossom_size = run_blossom(g, matching);
501 const size_t exact_size = exact_maximum_matching_size(12, edges);
502
506}
507
509{
510 using Graph = TypeParam;
511
512 // "Flower": three odd petals sharing vertex 0, connected to a tail.
513 // Petals:
514 // 0-1-2-0
515 // 0-3-4-0
516 // 0-5-6-0
517 // Tail:
518 // 2-7-8-9
520 {0, 1}, {1, 2}, {2, 0},
521 {0, 3}, {3, 4}, {4, 0},
522 {0, 5}, {5, 6}, {6, 0},
523 {2, 7}, {7, 8}, {8, 9}
524 };
525
526 auto g = build_graph<Graph>(10, edges);
528
529 const size_t blossom_size = run_blossom(g, matching);
530 const size_t exact_size = exact_maximum_matching_size(10, edges);
531
535}
536
538{
539 using Graph = TypeParam;
540
541 std::mt19937 rng(0xB10550u);
542 std::uniform_real_distribution<double> coin(0.0, 1.0);
543
544 // Stress sizes beyond exact-DP range.
545 for (int sample = 0; sample < 12; ++sample)
546 {
547 const size_t n = 30 + static_cast<size_t>(sample % 11); // [30, 40]
548 const double p = 0.16 + 0.01 * static_cast<double>(sample);
549
551 edges.reserve(n * (n - 1) / 2);
552 for (size_t u = 0; u < n; ++u)
553 for (size_t v = u + 1; v < n; ++v)
554 if (coin(rng) < p)
555 edges.append(std::make_pair(u, v));
556
557 auto g = build_graph<Graph>(n, edges);
559 const size_t blossom_size = run_blossom(g, matching);
560
562 << "sample=" << sample << " n=" << n;
564 << "sample=" << sample << " n=" << n;
565 EXPECT_LE(blossom_size, n / 2)
566 << "sample=" << sample << " n=" << n;
567 }
568}
569
571{
572 using Graph = TypeParam;
573
574 const char * run_perf = std::getenv("ALEPH_RUN_BLOSSOM_PERF");
575 if (run_perf == nullptr or std::string(run_perf) != "1")
576 GTEST_SKIP() << "Set ALEPH_RUN_BLOSSOM_PERF=1 to run blossom perf regression test";
577
578 constexpr size_t n = 900;
579 constexpr double p = 0.02;
580 constexpr unsigned seed = 0xB10550u;
581# ifdef NDEBUG
582 constexpr long long kBudgetMs = 12000;
583# else
584 constexpr long long kBudgetMs = 25000;
585# endif
586
587 std::mt19937 rng(seed);
588 std::uniform_real_distribution<double> coin(0.0, 1.0);
589
591 edges.reserve(static_cast<size_t>(n * n * p));
592 for (size_t u = 0; u < n; ++u)
593 for (size_t v = u + 1; v < n; ++v)
594 if (coin(rng) < p)
595 edges.append(std::make_pair(u, v));
596
597 if (edges.is_empty())
598 edges.append(std::make_pair(0, 1));
599
600 auto g = build_graph<Graph>(n, edges);
602
603 const auto t0 = std::chrono::steady_clock::now();
604 const size_t blossom_size = run_blossom(g, matching);
605 const auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
606 std::chrono::steady_clock::now() - t0).count();
607
610 EXPECT_LE(blossom_size, n / 2);
611 EXPECT_LT(elapsed_ms, kBudgetMs)
612 << "Blossom perf regression: n=" << n
613 << ", edges=" << edges.size()
614 << ", elapsed_ms=" << elapsed_ms
615 << ", budget_ms=" << kBudgetMs;
616}
617
618// -----------------------------------------------------------------------------
619// Dedicated non-typed tests
620// -----------------------------------------------------------------------------
621
622namespace
623{
624 struct Positive_Arc_Filter
625 {
626 template <class Arc>
627 bool operator()(Arc * a) const noexcept
628 {
629 return a->get_info() > 0;
630 }
631 };
632}
633
635{
637
638 Graph g;
639 auto * n0 = g.insert_node(0);
640 auto * n1 = g.insert_node(1);
641 auto * n2 = g.insert_node(2);
642 auto * n3 = g.insert_node(3);
643
644 // Positive arc.
645 g.insert_arc(n0, n1, 1);
646 // Zero arcs (filtered out).
647 g.insert_arc(n1, n2, 0);
648 g.insert_arc(n2, n3, 0);
649 g.insert_arc(n0, n3, 0);
650
652 const size_t cardinality =
654 g, matching, Positive_Arc_Filter());
655
656 EXPECT_EQ(cardinality, 1u);
657 EXPECT_EQ(matching.size(), 1u);
659}
660
662{
664
665 Digraph g;
666 auto * n0 = g.insert_node(0);
667 auto * n1 = g.insert_node(1);
668 g.insert_arc(n0, n1, 1);
669
671
673 std::domain_error);
674}
Edmonds' Blossom algorithm for maximum matching in general graphs.
#define ah_runtime_error_if(C)
Throws std::runtime_error if condition holds.
Definition ah-errors.H:266
TYPED_TEST_SUITE(AStarHeapTest, HeapTypes)
WeightedDigraph::Arc Arc
bool verify_matching(const Graph &g, const DynDlist< Graph::Arc * > &matching)
Verifies that a matching is valid:
TYPED_TEST(BlossomMatchingTypedTest, EmptyGraph)
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:139
Generic directed graph (digraph) wrapper template.
Definition graph-dry.H:3854
size_t size() const noexcept
Return the current dimension of array.
T & append()
Allocate a new entry to the end of array.
bool is_empty() const noexcept
Return true if the array is empty.
void reserve(const size_t l, const size_t r)
Allocate a range of entries.
Dynamic doubly linked list with O(1) size and bidirectional access.
Dynamic set backed by balanced binary search trees with automatic memory management.
Arc for graphs implemented with simple adjacency lists.
Definition tpl_sgraph.H:197
virtual Node * insert_node(Node *node) noexcept
Insertion of a node already allocated.
Definition tpl_graph.H:524
Arc * insert_arc(Node *src_node, Node *tgt_node, void *a)
Definition tpl_graph.H:604
Graph class implemented with singly-linked adjacency lists.
Definition tpl_sgraph.H:274
Open addressing hash table with linear probing collision resolution.
Definition tpl_olhash.H:170
NodeInfo & get_info() noexcept
Return a modifiable reference to the data contained in the node.
Definition graph-dry.H:494
Node * get_src_node(Arc *arc) const noexcept
Return the source node of arc (only for directed graphs)
Definition graph-dry.H:737
Node * get_tgt_node(Arc *arc) const noexcept
Return the target node of arc (only for directed graphs)
Definition graph-dry.H:743
void for_each_node(Operation &operation) const
Unconditionally traverse all the nodes of graph and on each one perform an operation.
Definition graph-dry.H:1482
Key * insert(const Key &key)
Inserts a key into the hash table (copy version).
Definition hashDry.H:203
#define TEST(name)
static mt19937 rng
DynArray< Graph::Node * > nodes
Definition graphpic.C:406
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
bool all(Container &container, Operation &operation)
Return true if all elements satisfy a predicate.
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 struct argp_option options[]
Definition ntreepic.C:1886
Arc of graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:222
ValueArg< size_t > seed
Definition testHash.C:48
gsl_rng * r
Array-based graph implementation.
Dynamic array container with automatic resizing.
Bipartite graph detection and 2-coloring.
Lazy and scalable dynamic array implementation.
Dynamic set implementations based on balanced binary search trees.
Generic graph and digraph implementations.
Open addressing hash table with linear probing.
Simple graph implementation with adjacency lists.
DynList< int > l