Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
branch_and_bound_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#include <memory>
32#include <stdexcept>
33#include <string>
34
35#include <gtest/gtest.h>
36
37#include <Knapsack.H>
38#include <State_Search.H>
39
40using namespace Aleph;
41
42namespace
43{
44
45struct ArtificialState
46{
47 size_t depth = 0;
48 int code = 1;
49 int value = 0;
50};
51
52struct ArtificialMove
53{
54 int target_code = 1;
55 int delta = 0;
56 char label = 'L';
57};
58
59struct ArtificialMaxDomain
60{
61 using State = ArtificialState;
62 using Move = ArtificialMove;
63 using Objective = int;
64
65 bool is_complete(const State &state) const
66 {
67 return state.depth == 2;
68 }
69
70 Objective objective_value(const State &state) const
71 {
72 return state.value;
73 }
74
75 Objective bound(const State &state) const
76 {
77 switch (state.code)
78 {
79 case 1: return 9;
80 case 2: return 9;
81 case 3: return 5;
82 default: return state.value;
83 }
84 }
85
86 void apply(State &state, const Move &move) const
87 {
88 state.code = move.target_code;
89 state.value += move.delta;
90 ++state.depth;
91 }
92
93 void undo(State &state, const Move &move) const
94 {
95 --state.depth;
96 state.value -= move.delta;
97 state.code /= 2;
98 }
99
100 template <typename Visitor>
101 bool for_each_successor(const State &state, Visitor visit) const
102 {
103 if (state.depth >= 2)
104 return true;
105
106 switch (state.code)
107 {
108 case 1:
109 if (not visit(Move{2, 0, 'L'}))
110 return false;
111 return visit(Move{3, 0, 'R'});
112
113 case 2:
114 if (not visit(Move{4, 7, 'L'}))
115 return false;
116 return visit(Move{5, 9, 'R'});
117
118 case 3:
119 if (not visit(Move{6, 5, 'L'}))
120 return false;
121 return visit(Move{7, 4, 'R'});
122
123 default:
124 return true;
125 }
126 }
127};
128
129struct ArtificialMaxBadOrderDomain
130{
131 using State = ArtificialState;
132 using Move = ArtificialMove;
133 using Objective = int;
134
135 bool is_complete(const State &state) const
136 {
137 return state.depth == 2;
138 }
139
140 Objective objective_value(const State &state) const
141 {
142 return state.value;
143 }
144
145 Objective bound(const State &state) const
146 {
147 switch (state.code)
148 {
149 case 1: return 9;
150 case 2: return 9;
151 case 3: return 5;
152 default: return state.value;
153 }
154 }
155
156 void apply(State &state, const Move &move) const
157 {
158 state.code = move.target_code;
159 state.value += move.delta;
160 ++state.depth;
161 }
162
163 void undo(State &state, const Move &move) const
164 {
165 --state.depth;
166 state.value -= move.delta;
167 state.code /= 2;
168 }
169
170 template <typename Visitor>
171 bool for_each_successor(const State &state, Visitor visit) const
172 {
173 if (state.depth >= 2)
174 return true;
175
176 switch (state.code)
177 {
178 case 1:
179 if (not visit(Move{3, 0, 'R'}))
180 return false;
181 return visit(Move{2, 0, 'L'});
182
183 case 2:
184 if (not visit(Move{4, 7, 'L'}))
185 return false;
186 return visit(Move{5, 9, 'R'});
187
188 case 3:
189 if (not visit(Move{6, 5, 'L'}))
190 return false;
191 return visit(Move{7, 4, 'R'});
192
193 default:
194 return true;
195 }
196 }
197};
198
199struct ArtificialMaxVisitedDomain : ArtificialMaxDomain
200{
201 using State_Key = int;
202
203 [[nodiscard]] State_Key state_key(const State &state) const noexcept
204 {
205 return state.code;
206 }
207};
208
209struct ArtificialMinDomain
210{
211 using State = ArtificialState;
212 using Move = ArtificialMove;
213 using Objective = int;
214
215 bool is_complete(const State &state) const
216 {
217 return state.depth == 2;
218 }
219
220 Objective objective_value(const State &state) const
221 {
222 return state.value;
223 }
224
225 Objective bound(const State &state) const
226 {
227 switch (state.code)
228 {
229 case 1: return 6;
230 case 2: return 6;
231 case 3: return 7;
232 default: return state.value;
233 }
234 }
235
236 void apply(State &state, const Move &move) const
237 {
238 state.code = move.target_code;
239 state.value += move.delta;
240 ++state.depth;
241 }
242
243 void undo(State &state, const Move &move) const
244 {
245 --state.depth;
246 state.value -= move.delta;
247 state.code /= 2;
248 }
249
250 template <typename Visitor>
251 bool for_each_successor(const State &state, Visitor visit) const
252 {
253 if (state.depth >= 2)
254 return true;
255
256 switch (state.code)
257 {
258 case 1:
259 if (not visit(Move{2, 0, 'L'}))
260 return false;
261 return visit(Move{3, 0, 'R'});
262
263 case 2:
264 if (not visit(Move{4, 8, 'L'}))
265 return false;
266 return visit(Move{5, 6, 'R'});
267
268 case 3:
269 if (not visit(Move{6, 7, 'L'}))
270 return false;
271 return visit(Move{7, 9, 'R'});
272
273 default:
274 return true;
275 }
276 }
277};
278
279struct KnapsackState
280{
281 size_t index = 0;
282 int weight = 0;
283 double value = 0;
285
286 explicit KnapsackState(const size_t n = 0)
287 : index(0), weight(0), value(0), chosen(n, 0)
288 {
289 // empty
290 }
291};
292
293class KnapsackBBDomain
294{
295public:
296 using State = KnapsackState;
297
298 struct Move
299 {
300 bool take = false;
301 };
302
303 using Objective = double;
304
305 KnapsackBBDomain(const Array<Knapsack_Item<int, double>> &items,
306 const int capacity,
307 const bool fractional_bound)
308 : items_(items),
309 suffix_values_(items.size() + 1, 0.0),
310 capacity_(capacity),
311 use_fractional_bound_(fractional_bound)
312 {
313 for (size_t i = items_.size(); i > 0; --i)
314 suffix_values_[i - 1] = suffix_values_[i] + items_[i - 1].value;
315 }
316
317 bool is_complete(const State &state) const
318 {
319 return state.index == items_.size();
320 }
321
322 Objective objective_value(const State &state) const
323 {
324 return state.value;
325 }
326
327 Objective bound(const State &state) const
328 {
329 if (not use_fractional_bound_)
330 return state.value + suffix_values_[state.index];
331
332 Objective optimistic = state.value;
333 int remaining = capacity_ - state.weight;
334
335 for (size_t i = state.index; i < items_.size() and remaining > 0; ++i)
336 if (items_[i].weight <= remaining)
337 {
338 optimistic += items_[i].value;
339 remaining -= items_[i].weight;
340 }
341 else
342 {
343 optimistic += items_[i].value * (static_cast<Objective>(remaining)/
344 static_cast<Objective>(items_[i].weight));
345 break;
346 }
347
348 return optimistic;
349 }
350
351 void apply(State &state, const Move &move) const
352 {
353 if (move.take)
354 {
355 state.weight += items_[state.index].weight;
356 state.value += items_[state.index].value;
357 state.chosen[state.index] = 1;
358 }
359 else
360 state.chosen[state.index] = 0;
361
362 ++state.index;
363 }
364
365 void undo(State &state, const Move &move) const
366 {
367 --state.index;
368 if (move.take)
369 {
370 state.weight -= items_[state.index].weight;
371 state.value -= items_[state.index].value;
372 }
373
374 state.chosen[state.index] = 0;
375 }
376
377 template <typename Visitor>
378 bool for_each_successor(const State &state, Visitor visit) const
379 {
380 if (state.index >= items_.size())
381 return true;
382
383 if (state.weight + items_[state.index].weight <= capacity_)
384 if (not visit(Move{true}))
385 return false;
386
387 return visit(Move{false});
388 }
389
390private:
392 Array<double> suffix_values_;
393 int capacity_ = 0;
394 bool use_fractional_bound_ = true;
395};
396
397struct AssignmentState
398{
399 size_t row = 0;
400 int total_cost = 0;
401 Array<unsigned char> used_columns;
402 Array<int> assigned_column;
403
404 explicit AssignmentState(const size_t n = 0)
405 : row(0), total_cost(0), used_columns(n, 0), assigned_column(n, -1)
406 {
407 // empty
408 }
409};
410
411class AssignmentBBDomain
412{
413public:
414 struct Move
415 {
416 size_t col = 0;
417 };
418
419 using State = AssignmentState;
420 using Objective = int;
421
422 explicit AssignmentBBDomain(const Array<Array<int>> &costs)
423 : costs_(costs)
424 {
425 // empty
426 }
427
428 bool is_complete(const State &state) const
429 {
430 return state.row == costs_.size();
431 }
432
433 Objective objective_value(const State &state) const
434 {
435 return state.total_cost;
436 }
437
438 Objective bound(const State &state) const
439 {
440 Objective optimistic = state.total_cost;
441
442 for (size_t row = state.row; row < costs_.size(); ++row)
443 {
444 int best = -1;
445 for (size_t col = 0; col < costs_[row].size(); ++col)
446 if (not state.used_columns[col] and (best < 0 or costs_[row][col] < best))
447 best = costs_[row][col];
448
449 optimistic += best;
450 }
451
452 return optimistic;
453 }
454
455 void apply(State &state, const Move &move) const
456 {
457 state.total_cost += costs_[state.row][move.col];
458 state.used_columns[move.col] = 1;
459 state.assigned_column[state.row] = static_cast<int>(move.col);
460 ++state.row;
461 }
462
463 void undo(State &state, const Move &move) const
464 {
465 --state.row;
466 state.total_cost -= costs_[state.row][move.col];
467 state.used_columns[move.col] = 0;
468 state.assigned_column[state.row] = -1;
469 }
470
471 template <typename Visitor>
472 bool for_each_successor(const State &state, Visitor visit) const
473 {
474 if (state.row >= costs_.size())
475 return true;
476
477 for (size_t col = 0; col < costs_[state.row].size(); ++col)
478 if (not state.used_columns[col] and not visit(Move{col}))
479 return false;
480
481 return true;
482 }
483
484private:
485 Array<Array<int>> costs_;
486};
487
488struct ThrowingApplyState
489{
490 std::shared_ptr<bool> undo_called;
491 size_t node = 0;
492};
493
494struct ThrowingApplyMove
495{
496 bool throws = true;
497};
498
499struct ThrowingApplyBBDomain
500{
501 using State = ThrowingApplyState;
502 using Move = ThrowingApplyMove;
503 using Objective = int;
504
505 bool is_complete(const State &state) const
506 {
507 return state.node == 1;
508 }
509
510 Objective objective_value(const State &) const
511 {
512 return 0;
513 }
514
515 Objective bound(const State &) const
516 {
517 return 1;
518 }
519
520 void apply(State &state, const Move &move) const
521 {
522 if (move.throws)
523 ah_runtime_error() << "apply failed";
524
525 state.node = 1;
526 }
527
528 void undo(State &state, const Move &) const
529 {
530 *state.undo_called = true;
531 state.node = 0;
532 }
533
534 template <typename Visitor>
535 bool for_each_successor(const State &state, Visitor visit) const
536 {
537 if (state.node != 0)
538 return true;
539
540 return visit(Move{true});
541 }
542};
543
544struct ThrowingApplyVisitedBBDomain : ThrowingApplyBBDomain
545{
546 using State_Key = size_t;
547
548 [[nodiscard]] State_Key state_key(const State &state) const noexcept
549 {
550 return state.node;
551 }
552};
553
563
565{
566 std::string out;
567 for (const auto &move : path)
568 out.push_back(move.label);
569 return out;
570}
571
573 const size_t row,
575{
576 if (row == costs.size())
577 return 0;
578
579 int best = -1;
580 for (size_t col = 0; col < costs[row].size(); ++col)
581 if (not used[col])
582 {
583 used[col] = 1;
584 const int candidate = costs[row][col] + brute_assignment_cost(costs, row + 1, used);
585 used[col] = 0;
586 if (best < 0 or candidate < best)
587 best = candidate;
588 }
589
590 return best;
591}
592
593// ---------------------------------------------------------------------------
594// Domain with IncrementalBoundProvider (bound_after) to exercise the fast
595// path in Branch_And_Bound::collect_ordered_moves.
596// Same tree shape as ArtificialMaxDomain.
597// ---------------------------------------------------------------------------
598struct IncrementalBoundDomain
599{
600 using State = ArtificialState;
601 using Move = ArtificialMove;
602 using Objective = int;
603
604 mutable size_t bound_after_calls = 0;
605 mutable size_t apply_calls = 0;
606
607 bool is_complete(const State &state) const
608 {
609 return state.depth == 2;
610 }
611
612 Objective objective_value(const State &state) const
613 {
614 return state.value;
615 }
616
617 Objective bound(const State &state) const
618 {
619 switch (state.code)
620 {
621 case 1: return 9;
622 case 2: return 9;
623 case 3: return 5;
624 default: return state.value;
625 }
626 }
627
628 Objective bound_after(const State &, const Move &move) const
629 {
630 ++bound_after_calls;
631 // Return the bound of the child state without mutating.
632 switch (move.target_code)
633 {
634 case 2: return 9;
635 case 3: return 5;
636 default: return move.delta;
637 }
638 }
639
640 void apply(State &state, const Move &move) const
641 {
642 ++apply_calls;
643 state.code = move.target_code;
644 state.value += move.delta;
645 ++state.depth;
646 }
647
648 void undo(State &state, const Move &move) const
649 {
650 --state.depth;
651 state.value -= move.delta;
652 state.code /= 2;
653 }
654
655 template <typename Visitor>
656 bool for_each_successor(const State &state, Visitor visit) const
657 {
658 if (state.depth >= 2)
659 return true;
660
661 switch (state.code)
662 {
663 case 1:
664 if (not visit(Move{2, 0, 'L'}))
665 return false;
666 return visit(Move{3, 0, 'R'});
667
668 case 2:
669 if (not visit(Move{4, 7, 'L'}))
670 return false;
671 return visit(Move{5, 9, 'R'});
672
673 case 3:
674 if (not visit(Move{6, 5, 'L'}))
675 return false;
676 return visit(Move{7, 4, 'R'});
677
678 default:
679 return true;
680 }
681 }
682};
683
684} // end namespace
685
687{
689
691 EXPECT_FALSE(incumbent.has_value());
692 EXPECT_TRUE(incumbent.can_improve(10));
693
694 Sol first;
695 first.objective_value = 5;
696 EXPECT_TRUE(incumbent.consider(first));
697 EXPECT_TRUE(incumbent.has_value());
698 EXPECT_EQ(incumbent.best_value(), 5);
699 EXPECT_FALSE(incumbent.can_improve(5));
700 EXPECT_TRUE(incumbent.can_improve(6));
701}
702
704{
705 ArtificialMaxDomain domain;
707
708 auto result = branch_and_bound_search(domain, ArtificialState{}, collector);
709
710 ASSERT_TRUE(result.found_solution());
711 EXPECT_TRUE(result.exhausted());
712 EXPECT_EQ(result.incumbent.best_value(), 9);
713 EXPECT_EQ(artificial_signature(result.incumbent.get().path), "LR");
714 EXPECT_EQ(result.stats.solutions_found, 2u);
715 EXPECT_EQ(result.stats.pruned_by_bound, 1u);
716 EXPECT_EQ(result.stats.incumbent_updates, 2u);
717 EXPECT_EQ(collector.size(), 2u);
718}
719
721{
722 ArtificialMaxVisitedDomain domain;
725
726 auto result = engine.search(ArtificialState{}, visited);
727
728 ASSERT_TRUE(result.found_solution());
729 EXPECT_NE(visited.search(1), nullptr);
730 EXPECT_NE(visited.search(2), nullptr);
731}
732
734{
735 ArtificialMinDomain domain;
736 ExplorationPolicy policy = Branch_And_Bound<ArtificialMinDomain,
737 Minimize_Objective<int>>::default_policy();
738 policy.strategy = ExplorationPolicy::Strategy::Best_First;
739
741 auto result = engine.search(ArtificialState{});
742
743 ASSERT_TRUE(result.found_solution());
744 EXPECT_TRUE(result.exhausted());
745 EXPECT_EQ(result.incumbent.best_value(), 6);
746 EXPECT_EQ(artificial_signature(result.incumbent.get().path), "LR");
747 EXPECT_GT(result.stats.pruned_by_bound, 0u);
748}
749
751{
752 ArtificialMaxBadOrderDomain domain;
753
754 auto plain_result = branch_and_bound_search(domain, ArtificialState{});
755
757 ordered_policy.move_ordering = MoveOrderingMode::Estimated_Bound;
758 auto ordered_result = branch_and_bound_search(domain, ArtificialState{}, ordered_policy);
759
761 best_policy.strategy = ExplorationPolicy::Strategy::Best_First;
762 auto best_result = branch_and_bound_search(domain, ArtificialState{}, best_policy);
763
764 ASSERT_TRUE(plain_result.found_solution());
765 ASSERT_TRUE(ordered_result.found_solution());
766 ASSERT_TRUE(best_result.found_solution());
767 EXPECT_EQ(plain_result.incumbent.best_value(), 9);
768 EXPECT_EQ(ordered_result.incumbent.best_value(), plain_result.incumbent.best_value());
769 EXPECT_EQ(best_result.incumbent.best_value(), plain_result.incumbent.best_value());
770 EXPECT_EQ(artificial_signature(ordered_result.incumbent.get().path), "LR");
771 EXPECT_LT(ordered_result.stats.visited_states, plain_result.stats.visited_states);
772 EXPECT_GT(ordered_result.stats.move_ordering.ordered_batches, 0u);
773 EXPECT_GT(ordered_result.stats.move_ordering.priority_estimates, 0u);
774 EXPECT_LT(best_result.stats.visited_states, plain_result.stats.visited_states);
775}
776
778{
779 auto undo_called = std::make_shared<bool>(false);
780 ThrowingApplyBBDomain domain;
782 policy.move_ordering = MoveOrderingMode::Estimated_Bound;
783
785
786 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}), std::runtime_error);
787 EXPECT_FALSE(*undo_called);
788}
789
791{
792 auto undo_called = std::make_shared<bool>(false);
793 ThrowingApplyBBDomain domain;
795
796 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}), std::runtime_error);
797 EXPECT_FALSE(*undo_called);
798}
799
801{
802 auto undo_called = std::make_shared<bool>(false);
803 ThrowingApplyVisitedBBDomain domain;
806
807 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}, visited),
808 std::runtime_error);
809 EXPECT_FALSE(*undo_called);
810}
811
812struct ThrowingPostApplyDomain : ThrowingApplyBBDomain
813{
814 bool is_complete(const State &) const
815 {
816 return false;
817 }
818
819 void apply(State &state, const Move &move) const
820 {
821 (void) move;
822 state.node = 1; // succeed
823 }
824
825 Objective bound(const State &state) const
826 {
827 if (state.node == 1)
828 ah_runtime_error() << "post-apply bound failed";
829 return 1;
830 }
831};
832
834{
835 auto undo_called = std::make_shared<bool>(false);
838
839 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}), std::runtime_error);
840 EXPECT_TRUE(*undo_called);
841}
842
844{
845 using State_Key = size_t;
846 [[nodiscard]] State_Key state_key(const State &state) const noexcept
847 {
848 return state.node;
849 }
850};
851
853{
854 auto undo_called = std::make_shared<bool>(false);
858
859 // First call should throw and rollback.
860 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}, visited), std::runtime_error);
861 EXPECT_TRUE(*undo_called);
862 EXPECT_FALSE(visited.contains(0)); // Root key 0 should have been rolled back.
863
864 // Retry with same visited map to ensure it's clean.
865 *undo_called = false;
866 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}, visited), std::runtime_error);
867 EXPECT_TRUE(*undo_called);
868 EXPECT_FALSE(visited.contains(0));
869}
870
872{
873 ArtificialMaxDomain domain;
875 policy.stop_at_first_solution = true;
876
877 auto result = branch_and_bound_search(domain, ArtificialState{}, policy);
878
879 ASSERT_TRUE(result.found_solution());
880 EXPECT_TRUE(result.stopped_on_solution());
881 EXPECT_EQ(result.stats.solutions_found, 1u);
882 EXPECT_EQ(result.incumbent.best_value(), 7);
883 EXPECT_EQ(artificial_signature(result.incumbent.get().path), "LL");
884}
885
887{
888 const Array<Knapsack_Item<int, double>> items = {
889 {2, 40.0}, {5, 30.0}, {10, 50.0}, {5, 10.0}
890 };
891 constexpr int capacity = 16;
892
893 KnapsackBBDomain domain(items, capacity, true);
894 auto result = branch_and_bound_search(domain, KnapsackState(items.size()));
895 const double optimum = knapsack_01_value<double, int>(items, capacity);
896
897 ASSERT_TRUE(result.found_solution());
898 EXPECT_DOUBLE_EQ(result.incumbent.best_value(), 90.0);
899 EXPECT_DOUBLE_EQ(result.incumbent.best_value(), optimum);
900}
901
903{
904 const Array<Knapsack_Item<int, double>> items = {
905 {2, 40.0}, {5, 30.0}, {10, 50.0}, {5, 10.0}
906 };
907 constexpr int capacity = 16;
908
909 KnapsackBBDomain domain(items, capacity, true);
911
913 policy.strategy = ExplorationPolicy::Strategy::Best_First;
915 auto best_result = best_first.search(KnapsackState(items.size()));
916
917 EXPECT_DOUBLE_EQ(depth_result.incumbent.best_value(), best_result.incumbent.best_value());
918 EXPECT_GT(best_result.stats.pruned_by_bound, 0u);
919}
920
922{
923 const Array<Knapsack_Item<int, double>> items = {
924 {10, 100.0}, {9, 80.0}, {9, 80.0}, {9, 80.0}
925 };
926 constexpr int capacity = 10;
927
928 auto tight_result = branch_and_bound_search(KnapsackBBDomain(items, capacity, true),
929 KnapsackState(items.size()));
930 auto weak_result = branch_and_bound_search(KnapsackBBDomain(items, capacity, false),
931 KnapsackState(items.size()));
932
933 EXPECT_DOUBLE_EQ(tight_result.incumbent.best_value(), weak_result.incumbent.best_value());
934 EXPECT_GT(tight_result.stats.pruned_by_bound, 0u);
935 EXPECT_LT(tight_result.stats.visited_states, weak_result.stats.visited_states);
936 EXPECT_LT(tight_result.stats.expanded_states, weak_result.stats.expanded_states);
937}
938
940{
941 const Array<Array<int>> costs = {
942 Array<int>{9, 2, 7, 8},
943 Array<int>{6, 4, 3, 7},
944 Array<int>{5, 8, 1, 8},
945 Array<int>{7, 6, 9, 4}
946 };
947
948 AssignmentBBDomain domain(costs);
950 domain,
951 Branch_And_Bound<AssignmentBBDomain, Minimize_Objective<int>>::default_policy(),
952 {},
954
955 auto result = engine.search(AssignmentState(costs.size()));
956
958 const int brute = brute_assignment_cost(costs, 0, used);
959
960 ASSERT_TRUE(result.found_solution());
961 EXPECT_EQ(result.incumbent.best_value(), brute);
962 EXPECT_EQ(result.incumbent.best_value(), 13);
963}
964
966{
967 const Array<Array<int>> costs = {
968 Array<int>{9, 2, 7, 8},
969 Array<int>{6, 4, 3, 7},
970 Array<int>{5, 8, 1, 8},
971 Array<int>{7, 6, 9, 4}
972 };
973
974 AssignmentBBDomain domain(costs);
976 domain,
977 Branch_And_Bound<AssignmentBBDomain, Minimize_Objective<int>>::default_policy(),
978 {},
980 auto depth_result = depth_engine.search(AssignmentState(costs.size()));
981
982 ExplorationPolicy policy = Branch_And_Bound<AssignmentBBDomain,
983 Minimize_Objective<int>>::default_policy();
984 policy.strategy = ExplorationPolicy::Strategy::Best_First;
986 domain, policy, {}, Minimize_Objective<int>{});
987 auto best_result = best_engine.search(AssignmentState(costs.size()));
988
989 EXPECT_EQ(depth_result.incumbent.best_value(), best_result.incumbent.best_value());
990 EXPECT_GE(best_result.stats.pruned_by_bound, 0u);
991}
992
993// ===========================================================================
994// H3: B&B with max_depth limit, max_expansions limit, knapsack capacity=0
995// ===========================================================================
996
997// max_depth=1: root is expanded (depth 0→1), but children at depth 1 are
998// pruned before expansion. Since leaves are at depth 2, no complete solution
999// is recorded.
1001{
1002 ArtificialMaxDomain domain;
1004 policy.stop_at_first_solution = false;
1005
1006 SearchLimits limits;
1007 limits.max_depth = 1;
1008
1009 Branch_And_Bound<ArtificialMaxDomain> engine(domain, policy, limits);
1010 auto result = engine.search(ArtificialState{});
1011
1012 EXPECT_FALSE(result.found_solution());
1013 EXPECT_GT(result.stats.pruned_by_depth, 0u);
1014}
1015
1016// max_expansions=1: only the root node is expanded. The two children are
1017// visited but not expanded, so no leaves are reached and no solution found.
1019{
1020 ArtificialMaxDomain domain;
1022 policy.stop_at_first_solution = false;
1023
1024 SearchLimits limits;
1025 limits.max_expansions = 1;
1026
1027 Branch_And_Bound<ArtificialMaxDomain> engine(domain, policy, limits);
1028 auto result = engine.search(ArtificialState{});
1029
1030 EXPECT_TRUE(result.limit_reached());
1031 EXPECT_EQ(result.stats.expanded_states, 1u);
1032 EXPECT_FALSE(result.found_solution());
1033}
1034
1035// Knapsack with capacity=0: no item fits, so the only complete solution is
1036// the empty selection with value=0.
1038{
1039 const Array<Knapsack_Item<int, double>> items = {
1040 {2, 40.0}, {5, 30.0}, {10, 50.0}
1041 };
1042 constexpr int capacity = 0;
1043
1044 KnapsackBBDomain domain(items, capacity, true);
1045 auto result = branch_and_bound_search(domain, KnapsackState(items.size()));
1046
1047 ASSERT_TRUE(result.found_solution());
1048 EXPECT_DOUBLE_EQ(result.incumbent.best_value(), 0.0);
1049}
1050
1051// ===========================================================================
1052// H4: IncrementalBoundProvider fast path in collect_ordered_moves
1053// ===========================================================================
1054
1056{
1057 IncrementalBoundDomain domain;
1058 ExplorationPolicy policy;
1059 policy.move_ordering = MoveOrderingMode::Estimated_Bound;
1060 policy.stop_at_first_solution = false;
1061
1063 auto result = engine.search(ArtificialState{});
1064
1065 // Same tree as ArtificialMaxDomain: optimal = 9.
1066 ASSERT_TRUE(result.found_solution());
1067 EXPECT_EQ(result.incumbent.best_value(), 9);
1068
1069 // bound_after must have been called (fast path taken).
1070 EXPECT_GT(engine.domain().bound_after_calls, 0u);
1071
1072 // Move ordering stats must reflect priority estimates.
1073 EXPECT_GT(result.stats.move_ordering.priority_estimates, 0u);
1074}
Classical knapsack problem variants (0/1, unbounded, bounded).
Umbrella header for the implicit state-space search framework.
#define ah_runtime_error()
Throws std::runtime_error unconditionally.
Definition ah-errors.H:282
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
Reusable branch-and-bound engine over implicit state spaces.
static ExplorationPolicy default_policy() noexcept
Return the default branch-and-bound exploration policy.
Result search(State initial_state)
Execute branch and bound and keep only the incumbent.
Global incumbent manager for branch and bound.
Collector that stores accepted solutions in an Aleph list.
#define TEST(name)
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
and
Check uniqueness with explicit hash + equality functors.
size_t size(Node *root) noexcept
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::string code(Node *root)
Compute a string with the Lukasiewicz`s word of a tree.
auto branch_and_bound_search(Domain domain, typename Domain::State initial_state, ExplorationPolicy policy=Branch_And_Bound< Domain, ObjectivePolicy >::default_policy(), SearchLimits limits={}, ObjectivePolicy objective={})
Convenience wrapper for one-shot branch and bound.
Exploration controls shared across engines.
bool stop_at_first_solution
Stop when the first goal is found.
Strategy strategy
Traversal strategy.
MoveOrderingMode move_ordering
Successor-ordering mode.
An item for knapsack problems.
Definition Knapsack.H:66
Objective policy for maximization problems.
Objective policy for minimization problems.
Snapshot of a complete optimization solution.
Hard bounds applied by the search engine.
size_t max_expansions
Maximum expanded states.
size_t max_depth
Maximum expansion depth.
void apply(State &state, const Move &move) const
bool is_complete(const State &) const
Objective bound(const State &state) const
State_Key state_key(const State &state) const noexcept
static mt19937 engine