Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
hungarian_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
48# include <gtest/gtest.h>
49# include <random>
50# include <set>
51# include <Hungarian.H>
52# include <tpl_dynMat.H>
53# include <tpl_mincost.H>
54
55using namespace std;
56using namespace Aleph;
57
58// ============================================================================
59// Helper: build a DynMatrix from a 2D initializer list
60// ============================================================================
61template <typename T>
63{
64 size_t m = rows.size();
65 size_t n = rows.begin()->size();
66 DynMatrix<T> mat(m, n, T{0});
67 mat.allocate();
68 size_t i = 0;
69 for (const auto & row : rows)
70 {
71 size_t j = 0;
72 for (const auto & val : row)
73 mat(i, j++) = val;
74 ++i;
75 }
76 return mat;
77}
78
79// Helper: compute the actual cost of an assignment using the cost matrix
80template <typename T>
82 const DynList<pair<size_t, size_t>> & pairs)
83{
84 T total = T{0};
85 for (auto it = pairs.get_it(); it.has_curr(); it.next())
86 {
87 auto [r, c] = it.get_curr();
88 total += cost.read(r, c);
89 }
90 return total;
91}
92
93// Helper: verify the assignment is a valid permutation
94void verify_permutation(const Array<long> & row_to_col,
95 const Array<long> & col_to_row,
96 size_t rows, size_t cols)
97{
98 // Each assigned row maps to a unique column
100 for (size_t i = 0; i < rows; ++i)
101 {
102 long j = row_to_col[i];
103 if (j >= 0)
104 {
105 EXPECT_LT(static_cast<size_t>(j), cols)
106 << "Row " << i << " assigned to out-of-range column " << j;
107 EXPECT_TRUE(assigned_cols.insert(j).second)
108 << "Column " << j << " assigned to multiple rows";
109 }
110 }
111
112 // Each assigned column maps back correctly
113 for (size_t j = 0; j < cols; ++j)
114 {
115 long i = col_to_row[j];
116 if (i >= 0)
117 {
118 EXPECT_LT(static_cast<size_t>(i), rows)
119 << "Column " << j << " assigned to out-of-range row " << i;
120 EXPECT_EQ(row_to_col[static_cast<size_t>(i)],
121 static_cast<long>(j))
122 << "Inconsistent row/col mapping at row=" << i << " col=" << j;
123 }
124 }
125}
126
127// ============================================================================
128// HungarianBasic: small matrices with known solutions
129// ============================================================================
130
132{
133 auto mat = make_matrix<int>({{42}});
135
136 EXPECT_EQ(ha.get_total_cost(), 42);
137 EXPECT_EQ(ha.get_assignment(0), 0);
138 EXPECT_EQ(ha.rows(), 1u);
139 EXPECT_EQ(ha.cols(), 1u);
140}
141
143{
144 // Cost matrix:
145 // 1 2
146 // 3 0
147 // Optimal: (0,0)=1, (1,1)=0 -> cost=1
148 auto mat = make_matrix<int>({{1, 2}, {3, 0}});
150
151 EXPECT_EQ(ha.get_total_cost(), 1);
152
153 auto pairs = ha.get_assignments();
154 EXPECT_EQ(compute_assignment_cost(mat, pairs), 1);
155}
156
158{
159 // Classic example:
160 // 10 5 13
161 // 3 9 18
162 // 10 6 12
163 // Optimal: (0,1)=5, (1,0)=3, (2,2)=12 -> cost=20
164 auto mat = make_matrix<int>({{10, 5, 13}, {3, 9, 18}, {10, 6, 12}});
166
167 EXPECT_EQ(ha.get_total_cost(), 20);
168}
169
171{
172 // Example:
173 // 82 83 69 92
174 // 77 37 49 92
175 // 11 69 5 86
176 // 8 9 98 23
177 // Optimal: (0,2)=69, (1,1)=37, (2,0)=11, (3,3)=23 -> cost=140
178 auto mat = make_matrix<int>({
179 {82, 83, 69, 92},
180 {77, 37, 49, 92},
181 {11, 69, 5, 86},
182 { 8, 9, 98, 23}
183 });
185
186 EXPECT_EQ(ha.get_total_cost(), 140);
187
188 auto pairs = ha.get_assignments();
189 EXPECT_EQ(compute_assignment_cost(mat, pairs), 140);
190}
191
193{
194 // Rank-1 matrix c[i][j] = (i+1)*(j+1):
195 // 1 2 3 4
196 // 2 4 6 8
197 // 3 6 9 12
198 // 4 8 12 16
199 // Optimal: anti-diagonal (3,2,1,0) -> 4+6+6+4 = 20
200 auto mat = make_matrix<int>({
201 {1, 2, 3, 4},
202 {2, 4, 6, 8},
203 {3, 6, 9, 12},
204 {4, 8, 12, 16}
205 });
207
208 EXPECT_EQ(ha.get_total_cost(), 20);
209}
210
211// ============================================================================
212// HungarianRectangular: non-square matrices
213// ============================================================================
214
216{
217 // 2 workers, 3 tasks. One task will be unassigned.
218 // 1 2 3
219 // 4 5 6
220 // Optimal: assign to minimize cost -> (0,0)=1, (1,1)=5 -> cost=6
221 auto mat = make_matrix<int>({{1, 2, 3}, {4, 5, 6}});
223
224 EXPECT_EQ(ha.get_total_cost(), 6);
225
226 auto pairs = ha.get_assignments();
227 EXPECT_EQ(pairs.size(), 2u); // 2 pairs (one per row)
228}
229
231{
232 // 3 workers, 2 tasks. One worker will be unassigned.
233 // 1 4
234 // 2 5
235 // 3 6
236 // Optimal: (0,0)=1, (1,1)=5 -> cost=6
237 auto mat = make_matrix<int>({{1, 4}, {2, 5}, {3, 6}});
239
240 EXPECT_EQ(ha.get_total_cost(), 6);
241
242 auto pairs = ha.get_assignments();
243 EXPECT_EQ(pairs.size(), 2u); // min(rows, cols) assignments
244}
245
247{
248 // 10 3 7 2 8
249 // 5 9 1 6 4
250 // Optimal: (0,3)=2, (1,2)=1 -> cost=3
251 auto mat = make_matrix<int>({{10, 3, 7, 2, 8}, {5, 9, 1, 6, 4}});
253
254 EXPECT_EQ(ha.get_total_cost(), 3);
255}
256
258{
259 // 10 5
260 // 3 9
261 // 7 1
262 // 2 6
263 // 8 4
264 // Optimal: (3,0)=2, (2,1)=1 -> cost=3
265 auto mat = make_matrix<int>({{10, 5}, {3, 9}, {7, 1}, {2, 6}, {8, 4}});
267
268 EXPECT_EQ(ha.get_total_cost(), 3);
269}
270
271// ============================================================================
272// HungarianDegenerate: edge cases
273// ============================================================================
274
276{
277 auto mat = make_matrix<int>({{0, 0, 0}, {0, 0, 0}, {0, 0, 0}});
279
280 EXPECT_EQ(ha.get_total_cost(), 0);
281}
282
284{
285 auto mat = make_matrix<int>({{7, 7, 7}, {7, 7, 7}, {7, 7, 7}});
287
288 EXPECT_EQ(ha.get_total_cost(), 21);
289}
290
292{
293 // Diagonal is zero, off-diagonal is large
294 auto mat = make_matrix<int>({{0, 99, 99}, {99, 0, 99}, {99, 99, 0}});
296
297 EXPECT_EQ(ha.get_total_cost(), 0);
298
299 EXPECT_EQ(ha.get_assignment(0), 0);
300 EXPECT_EQ(ha.get_assignment(1), 1);
301 EXPECT_EQ(ha.get_assignment(2), 2);
302}
303
305{
306 // Cost = identity matrix
307 auto mat = make_matrix<int>({{1, 0, 0}, {0, 1, 0}, {0, 0, 1}});
309
310 EXPECT_EQ(ha.get_total_cost(), 0);
311}
312
314{
315 auto mat = make_matrix<int>({{5, 3, 8, 1}});
317
318 EXPECT_EQ(ha.get_total_cost(), 1);
319
320 auto pairs = ha.get_assignments();
321 EXPECT_EQ(pairs.size(), 1u);
322}
323
325{
326 auto mat = make_matrix<int>({{5}, {3}, {8}, {1}});
328
329 EXPECT_EQ(ha.get_total_cost(), 1);
330
331 auto pairs = ha.get_assignments();
332 EXPECT_EQ(pairs.size(), 1u);
333}
334
335// ============================================================================
336// HungarianNumerics: different numeric types and negative costs
337// ============================================================================
338
340{
341 auto mat = make_matrix<int>({{10, 5, 13}, {3, 9, 18}, {10, 6, 12}});
343 EXPECT_EQ(ha.get_total_cost(), 20);
344}
345
347{
348 auto mat = make_matrix<double>({{1.5, 2.5}, {3.5, 0.5}});
350 EXPECT_DOUBLE_EQ(ha.get_total_cost(), 2.0);
351}
352
354{
355 // All negative costs
356 auto mat = make_matrix<int>({{-1, -2, -3}, {-4, -5, -6}, {-7, -8, -9}});
358
359 // Minimum cost: (0,2)=-3, (1,1)=-5, (2,0)=-7 or similar
360 // The minimum sum is -3 + -5 + -7 = -15? Let's check:
361 // Actually: (0,2)=-3, (1,0)=-4, (2,1)=-8 = -15
362 // Or: (0,0)=-1, (1,1)=-5, (2,2)=-9 = -15
363 // Or: (0,1)=-2, (1,2)=-6, (2,0)=-7 = -15
364 // All permutations give -15 since c[i][j] = -(3*i + j + 1)
365 // Actually no. Let me recompute:
366 // row 0: -1 -2 -3
367 // row 1: -4 -5 -6
368 // row 2: -7 -8 -9
369 // Minimum cost assignment minimizes sum:
370 // (0,2)=-3, (1,1)=-5, (2,0)=-7: sum=-15
371 // (0,0)=-1, (1,1)=-5, (2,2)=-9: sum=-15
372 // (0,1)=-2, (1,0)=-4, (2,2)=-9: sum=-15
373 // All give -15. This matrix has all-equal permutation sums.
374 EXPECT_EQ(ha.get_total_cost(), -15);
375}
376
378{
379 auto mat = make_matrix<int>({{-5, 3}, {2, -7}});
381
382 // Options: (0,0)=-5 + (1,1)=-7 = -12 or (0,1)=3 + (1,0)=2 = 5
383 EXPECT_EQ(ha.get_total_cost(), -12);
384}
385
387{
388 auto mat = make_matrix<long>({{1000000, 2000000}, {3000000, 500000}});
390
391 // (0,0)=1000000 + (1,1)=500000 = 1500000
392 EXPECT_EQ(ha.get_total_cost(), 1500000L);
393}
394
395// ============================================================================
396// HungarianMaximization: maximize profit
397// ============================================================================
398
400{
401 auto mat = make_matrix<int>({{10, 5, 13}, {3, 9, 18}, {10, 6, 12}});
402 auto result = hungarian_max_assignment(mat);
403
404 // Maximum: (0,2)=13, (1,1)=9, (2,0)=10 = 32
405 // Or: (0,0)=10, (1,2)=18, (2,1)=6 = 34
406 // Let's check all 6 permutations:
407 // (0,1,2): 10+9+12=31
408 // (0,2,1): 10+18+6=34
409 // (1,0,2): 5+3+12=20
410 // (1,2,0): 5+18+10=33
411 // (2,0,1): 13+3+6=22
412 // (2,1,0): 13+9+10=32
413 // Max is 34
414 EXPECT_EQ(result.total_cost, 34);
415}
416
418{
419 auto mat = make_matrix<int>({{1, 2, 3}, {4, 5, 6}});
420 auto result = hungarian_max_assignment(mat);
421
422 // Maximum: (0,1)=2, (1,2)=6 = 8 or (0,2)=3, (1,1)=5 = 8
423 EXPECT_EQ(result.total_cost, 8);
424}
425
426// ============================================================================
427// HungarianAPI: interface correctness
428// ============================================================================
429
431{
433 {10, 5, 13},
434 {3, 9, 18},
435 {10, 6, 12}
436 });
437
438 EXPECT_EQ(ha.get_total_cost(), 20);
439 EXPECT_EQ(ha.rows(), 3u);
440 EXPECT_EQ(ha.cols(), 3u);
441 EXPECT_EQ(ha.dimension(), 3u);
442}
443
445{
446 auto mat = make_matrix<int>({{10, 5, 13}, {3, 9, 18}, {10, 6, 12}});
447 auto result = hungarian_assignment(mat);
448
449 EXPECT_EQ(result.total_cost, 20);
450 EXPECT_EQ(result.orig_rows, 3u);
451 EXPECT_EQ(result.orig_cols, 3u);
452
453 auto pairs = result.get_pairs();
454 EXPECT_EQ(pairs.size(), 3u);
455}
456
458{
459 // Rectangular 2x4: only 2 real assignments
460 auto mat = make_matrix<int>({{1, 2, 3, 4}, {5, 6, 7, 8}});
462
463 auto pairs = ha.get_assignments();
464 EXPECT_EQ(pairs.size(), 2u);
465
466 // Verify all pairs are within original dimensions
467 for (auto it = pairs.get_it(); it.has_curr(); it.next())
468 {
469 auto [r, c] = it.get_curr();
470 EXPECT_LT(r, 2u);
471 EXPECT_LT(c, 4u);
472 }
473}
474
476{
477 auto mat = make_matrix<int>({
478 {82, 83, 69, 92},
479 {77, 37, 49, 92},
480 {11, 69, 5, 86},
481 { 8, 9, 98, 23}
482 });
484
485 verify_permutation(ha.get_row_assignments(), ha.get_col_assignments(),
486 ha.rows(), ha.cols());
487
488 // Verify cost matches sum of assigned entries
489 auto pairs = ha.get_assignments();
490 EXPECT_EQ(compute_assignment_cost(mat, pairs), ha.get_total_cost());
491}
492
493// ============================================================================
494// HungarianValidation: permutation and cost correctness
495// ============================================================================
496
498{
499 // Every valid assignment is a permutation (for square matrices)
500 auto mat = make_matrix<int>({
501 {5, 9, 1},
502 {10, 3, 2},
503 {8, 7, 4}
504 });
506
508 for (size_t i = 0; i < 3; ++i)
509 {
510 long col = ha.get_assignment(i);
511 EXPECT_GE(col, 0);
512 EXPECT_LT(col, 3);
513 EXPECT_TRUE(assigned.insert(col).second)
514 << "Column " << col << " assigned twice";
515 }
516}
517
519{
520 auto mat = make_matrix<int>({
521 {25, 40, 35},
522 {40, 60, 35},
523 {20, 40, 25}
524 });
526
527 auto pairs = ha.get_assignments();
528 int actual = compute_assignment_cost(mat, pairs);
529 EXPECT_EQ(actual, ha.get_total_cost());
530}
531
533{
534 // Verify against brute-force for a 3x3 matrix
535 auto mat = make_matrix<int>({
536 {7, 3, 5},
537 {2, 8, 1},
538 {6, 4, 9}
539 });
540
541 // All 6 permutations of {0,1,2}
542 int perms[6][3] = {
543 {0,1,2}, {0,2,1}, {1,0,2}, {1,2,0}, {2,0,1}, {2,1,0}
544 };
545
546 int min_cost = numeric_limits<int>::max();
547 for (auto & perm : perms)
548 {
549 int cost = 0;
550 for (int i = 0; i < 3; ++i)
551 cost += static_cast<int>(mat.read(i, perm[i]));
552 min_cost = min(min_cost, cost);
553 }
554
556 EXPECT_EQ(ha.get_total_cost(), min_cost);
557}
558
560{
561 auto mat = make_matrix<int>({
562 {15, 4, 8, 12},
563 {7, 9, 3, 6},
564 {10, 14, 2, 11},
565 {5, 13, 1, 16}
566 });
567
568 // Brute-force: enumerate all 24 permutations
569 int perm[4] = {0, 1, 2, 3};
570 int min_cost = numeric_limits<int>::max();
571 do
572 {
573 int cost = 0;
574 for (int i = 0; i < 4; ++i)
575 cost += static_cast<int>(mat.read(i, perm[i]));
576 min_cost = min(min_cost, cost);
577 }
578 while (next_permutation(perm, perm + 4));
579
581 EXPECT_EQ(ha.get_total_cost(), min_cost);
582}
583
584// ============================================================================
585// HungarianStress: random large matrices
586// ============================================================================
587
589{
590 mt19937 rng(42);
592
593 DynMatrix<int> mat(10, 10, 0);
594 mat.allocate();
595 for (size_t i = 0; i < 10; ++i)
596 for (size_t j = 0; j < 10; ++j)
597 mat(i, j) = dist(rng);
598
600 verify_permutation(ha.get_row_assignments(), ha.get_col_assignments(),
601 10, 10);
602
603 auto pairs = ha.get_assignments();
604 EXPECT_EQ(pairs.size(), 10u);
605 EXPECT_EQ(compute_assignment_cost(mat, pairs), ha.get_total_cost());
606}
607
609{
610 mt19937 rng(123);
611 uniform_int_distribution<int> dist(1, 1000);
612
613 DynMatrix<int> mat(50, 50, 0);
614 mat.allocate();
615 for (size_t i = 0; i < 50; ++i)
616 for (size_t j = 0; j < 50; ++j)
617 mat(i, j) = dist(rng);
618
620 verify_permutation(ha.get_row_assignments(), ha.get_col_assignments(),
621 50, 50);
622
623 auto pairs = ha.get_assignments();
624 EXPECT_EQ(pairs.size(), 50u);
625 EXPECT_EQ(compute_assignment_cost(mat, pairs), ha.get_total_cost());
626}
627
629{
630 mt19937 rng(456);
631 uniform_int_distribution<int> dist(0, 10000);
632
633 DynMatrix<int> mat(100, 100, 0);
634 mat.allocate();
635 for (size_t i = 0; i < 100; ++i)
636 for (size_t j = 0; j < 100; ++j)
637 mat(i, j) = dist(rng);
638
640 verify_permutation(ha.get_row_assignments(), ha.get_col_assignments(),
641 100, 100);
642
643 auto pairs = ha.get_assignments();
644 EXPECT_EQ(pairs.size(), 100u);
645 EXPECT_EQ(compute_assignment_cost(mat, pairs), ha.get_total_cost());
646}
647
649{
650 mt19937 rng(789);
652
653 DynMatrix<int> mat(30, 50, 0);
654 mat.allocate();
655 for (size_t i = 0; i < 30; ++i)
656 for (size_t j = 0; j < 50; ++j)
657 mat(i, j) = dist(rng);
658
660 verify_permutation(ha.get_row_assignments(), ha.get_col_assignments(),
661 30, 50);
662
663 auto pairs = ha.get_assignments();
664 EXPECT_EQ(pairs.size(), 30u);
665 EXPECT_EQ(compute_assignment_cost(mat, pairs), ha.get_total_cost());
666}
667
669{
670 // Cross-validate with solve_assignment() from tpl_mincost.H
671 // for a small matrix where we can use both approaches
672 auto mat = make_matrix<int>({
673 {82, 83, 69, 92},
674 {77, 37, 49, 92},
675 {11, 69, 5, 86},
676 { 8, 9, 98, 23}
677 });
678
680
681 // Build std::vector for solve_assignment
683 {82, 83, 69, 92},
684 {77, 37, 49, 92},
685 {11, 69, 5, 86},
686 { 8, 9, 98, 23}
687 };
688
690 ASSERT_TRUE(flow_result.feasible);
691
692 EXPECT_EQ(ha.get_total_cost(), static_cast<int>(flow_result.total_cost));
693}
694
696{
697 mt19937 rng(999);
699
700 const size_t N = 20;
701
702 DynMatrix<int> mat(N, N, 0);
703 mat.allocate();
704 vector<vector<double>> std_costs(N, vector<double>(N));
705
706 for (size_t i = 0; i < N; ++i)
707 for (size_t j = 0; j < N; ++j)
708 {
709 int val = dist(rng);
710 mat(i, j) = val;
711 std_costs[i][j] = static_cast<double>(val);
712 }
713
716
717 ASSERT_TRUE(flow_result.feasible);
718 EXPECT_EQ(ha.get_total_cost(), static_cast<int>(flow_result.total_cost));
719}
Hungarian (Munkres) algorithm for the assignment problem.
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:139
Dynamic singly linked list with functional programming support.
Definition htlist.H:1155
Dynamic matrix with sparse storage.
Definition tpl_dynMat.H:120
const T & read(const size_t i, const size_t j) const
Read the entry at position (i, j).
Definition tpl_dynMat.H:452
void allocate()
Pre-allocate memory for the entire matrix.
Definition tpl_dynMat.H:249
Hungarian (Munkres) algorithm for optimal assignment.
Definition Hungarian.H:176
constexpr size_t size() const noexcept
Returns the number of entries in the table.
Definition hashDry.H:619
#define TEST(name)
static mt19937 rng
#define N
Definition fib.C:294
__gmp_expr< typename __gmp_resolve_expr< T, V >::value_type, __gmp_binary_expr< __gmp_expr< T, U >, __gmp_expr< V, W >, __gmp_min_function > > min(const __gmp_expr< T, U > &expr1, const __gmp_expr< V, W > &expr2)
Definition gmpfrxx.h:4111
Hungarian_Result< Cost_Type > hungarian_max_assignment(const DynMatrix< Cost_Type > &cost)
Compute maximum-profit assignment (free function).
Definition Hungarian.H:499
Hungarian_Result< Cost_Type > hungarian_assignment(const DynMatrix< Cost_Type > &cost)
Compute minimum-cost assignment (free function).
Definition Hungarian.H:461
DynMatrix< T > make_matrix(initializer_list< initializer_list< T > > rows)
void verify_permutation(const Array< long > &row_to_col, const Array< long > &col_to_row, size_t rows, size_t cols)
T compute_assignment_cost(const DynMatrix< T > &cost, const DynList< pair< size_t, size_t > > &pairs)
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
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.
std::decay_t< typename HeadC::Item_Type > T
Definition ah-zip.H:105
bool next_permutation(Array< T > &a, Compare cmp=Compare(), const bool reset_on_last=true)
Compute the next lexicographic permutation of an Array.
Definition ah-comb.H:834
std::pair< First, Second > pair
Alias to std::pair kept for backwards compatibility.
Definition ahPair.H:89
AssignmentResult< Cost_Type > solve_assignment(const std::vector< std::vector< Cost_Type > > &costs)
Solve the assignment problem using min-cost flow.
STL namespace.
FooMap m(5, fst_unit_pair_hash, snd_unit_pair_hash)
gsl_rng * r
Dynamic matrix with lazy allocation.
Advanced minimum cost flow algorithms.