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
564// The engine default-builds its solution snapshots: a State without a default
565// constructor used to fail inside Branch_And_Bound.H. Only that clause fails.
566struct NoDefaultBBState
567{
568 size_t depth;
569 explicit NoDefaultBBState(const size_t d) : depth(d) {}
570};
571
572struct NoDefaultStateBBDomain
573{
574 using State = NoDefaultBBState;
575 using Move = ArtificialMove;
576 using Objective = int;
577
578 bool is_complete(const State &state) const { return state.depth == 1; }
579 Objective objective_value(const State &) const { return 0; }
580 Objective bound(const State &) const { return 0; }
581 void apply(State &state, const Move &) const { ++state.depth; }
582 void undo(State &state, const Move &) const { --state.depth; }
583
584 template <typename Visitor>
585 bool for_each_successor(const State &, Visitor) const { return true; }
586};
587
592
594{
595 std::string out;
596 for (const auto &move : path)
597 out.push_back(move.label);
598 return out;
599}
600
602 const size_t row,
604{
605 if (row == costs.size())
606 return 0;
607
608 int best = -1;
609 for (size_t col = 0; col < costs[row].size(); ++col)
610 if (not used[col])
611 {
612 used[col] = 1;
613 const int candidate = costs[row][col] + brute_assignment_cost(costs, row + 1, used);
614 used[col] = 0;
615 if (best < 0 or candidate < best)
616 best = candidate;
617 }
618
619 return best;
620}
621
622// ---------------------------------------------------------------------------
623// Domain with IncrementalBoundProvider (bound_after) to exercise the fast
624// path in Branch_And_Bound::collect_ordered_moves.
625// Same tree shape as ArtificialMaxDomain.
626// ---------------------------------------------------------------------------
627struct IncrementalBoundDomain
628{
629 using State = ArtificialState;
630 using Move = ArtificialMove;
631 using Objective = int;
632
633 mutable size_t bound_after_calls = 0;
634 mutable size_t apply_calls = 0;
635
636 bool is_complete(const State &state) const
637 {
638 return state.depth == 2;
639 }
640
641 Objective objective_value(const State &state) const
642 {
643 return state.value;
644 }
645
646 Objective bound(const State &state) const
647 {
648 switch (state.code)
649 {
650 case 1: return 9;
651 case 2: return 9;
652 case 3: return 5;
653 default: return state.value;
654 }
655 }
656
657 Objective bound_after(const State &, const Move &move) const
658 {
659 ++bound_after_calls;
660 // Return the bound of the child state without mutating.
661 switch (move.target_code)
662 {
663 case 2: return 9;
664 case 3: return 5;
665 default: return move.delta;
666 }
667 }
668
669 void apply(State &state, const Move &move) const
670 {
671 ++apply_calls;
672 state.code = move.target_code;
673 state.value += move.delta;
674 ++state.depth;
675 }
676
677 void undo(State &state, const Move &move) const
678 {
679 --state.depth;
680 state.value -= move.delta;
681 state.code /= 2;
682 }
683
684 template <typename Visitor>
685 bool for_each_successor(const State &state, Visitor visit) const
686 {
687 if (state.depth >= 2)
688 return true;
689
690 switch (state.code)
691 {
692 case 1:
693 if (not visit(Move{2, 0, 'L'}))
694 return false;
695 return visit(Move{3, 0, 'R'});
696
697 case 2:
698 if (not visit(Move{4, 7, 'L'}))
699 return false;
700 return visit(Move{5, 9, 'R'});
701
702 case 3:
703 if (not visit(Move{6, 5, 'L'}))
704 return false;
705 return visit(Move{7, 4, 'R'});
706
707 default:
708 return true;
709 }
710 }
711};
712
713} // end namespace
714
716{
718
720 EXPECT_FALSE(incumbent.has_value());
721 EXPECT_TRUE(incumbent.can_improve(10));
722
723 Sol first;
724 first.objective_value = 5;
725 EXPECT_TRUE(incumbent.consider(first));
726 EXPECT_TRUE(incumbent.has_value());
727 EXPECT_EQ(incumbent.best_value(), 5);
728 EXPECT_FALSE(incumbent.can_improve(5));
729 EXPECT_TRUE(incumbent.can_improve(6));
730}
731
733{
734 ArtificialMaxDomain domain;
736
737 auto result = branch_and_bound_search(domain, ArtificialState{}, collector);
738
739 ASSERT_TRUE(result.found_solution());
740 EXPECT_TRUE(result.exhausted());
741 EXPECT_EQ(result.incumbent.best_value(), 9);
742 EXPECT_EQ(artificial_signature(result.incumbent.get().path), "LR");
743 EXPECT_EQ(result.stats.solutions_found, 2u);
744 EXPECT_EQ(result.stats.pruned_by_bound, 1u);
745 EXPECT_EQ(result.stats.incumbent_updates, 2u);
746 EXPECT_EQ(collector.size(), 2u);
747}
748
750{
751 ArtificialMaxVisitedDomain domain;
754
755 auto result = engine.search(ArtificialState{}, visited);
756
757 ASSERT_TRUE(result.found_solution());
758 EXPECT_NE(visited.search(1), nullptr);
759 EXPECT_NE(visited.search(2), nullptr);
760}
761
763{
764 ArtificialMinDomain domain;
765 ExplorationPolicy policy = Branch_And_Bound<ArtificialMinDomain,
766 Minimize_Objective<int>>::default_policy();
767 policy.strategy = ExplorationPolicy::Strategy::Best_First;
768
770 auto result = engine.search(ArtificialState{});
771
772 ASSERT_TRUE(result.found_solution());
773 EXPECT_TRUE(result.exhausted());
774 EXPECT_EQ(result.incumbent.best_value(), 6);
775 EXPECT_EQ(artificial_signature(result.incumbent.get().path), "LR");
776 EXPECT_GT(result.stats.pruned_by_bound, 0u);
777}
778
780{
781 ArtificialMaxBadOrderDomain domain;
782
783 auto plain_result = branch_and_bound_search(domain, ArtificialState{});
784
786 ordered_policy.move_ordering = MoveOrderingMode::Estimated_Bound;
787 auto ordered_result = branch_and_bound_search(domain, ArtificialState{}, ordered_policy);
788
790 best_policy.strategy = ExplorationPolicy::Strategy::Best_First;
791 auto best_result = branch_and_bound_search(domain, ArtificialState{}, best_policy);
792
793 ASSERT_TRUE(plain_result.found_solution());
794 ASSERT_TRUE(ordered_result.found_solution());
795 ASSERT_TRUE(best_result.found_solution());
796 EXPECT_EQ(plain_result.incumbent.best_value(), 9);
797 EXPECT_EQ(ordered_result.incumbent.best_value(), plain_result.incumbent.best_value());
798 EXPECT_EQ(best_result.incumbent.best_value(), plain_result.incumbent.best_value());
799 EXPECT_EQ(artificial_signature(ordered_result.incumbent.get().path), "LR");
800 EXPECT_LT(ordered_result.stats.visited_states, plain_result.stats.visited_states);
801 EXPECT_GT(ordered_result.stats.move_ordering.ordered_batches, 0u);
802 EXPECT_GT(ordered_result.stats.move_ordering.priority_estimates, 0u);
803 EXPECT_LT(best_result.stats.visited_states, plain_result.stats.visited_states);
804}
805
807{
808 auto undo_called = std::make_shared<bool>(false);
809 ThrowingApplyBBDomain domain;
811 policy.move_ordering = MoveOrderingMode::Estimated_Bound;
812
814
815 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}), std::runtime_error);
816 EXPECT_FALSE(*undo_called);
817}
818
820{
821 auto undo_called = std::make_shared<bool>(false);
822 ThrowingApplyBBDomain domain;
824
825 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}), std::runtime_error);
826 EXPECT_FALSE(*undo_called);
827}
828
830{
831 auto undo_called = std::make_shared<bool>(false);
832 ThrowingApplyVisitedBBDomain domain;
835
836 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}, visited),
837 std::runtime_error);
838 EXPECT_FALSE(*undo_called);
839}
840
841struct ThrowingPostApplyDomain : ThrowingApplyBBDomain
842{
843 bool is_complete(const State &) const
844 {
845 return false;
846 }
847
848 void apply(State &state, const Move &move) const
849 {
850 (void) move;
851 state.node = 1; // succeed
852 }
853
854 Objective bound(const State &state) const
855 {
856 if (state.node == 1)
857 ah_runtime_error() << "post-apply bound failed";
858 return 1;
859 }
860};
861
863{
864 auto undo_called = std::make_shared<bool>(false);
867
868 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}), std::runtime_error);
869 EXPECT_TRUE(*undo_called);
870}
871
873{
874 using State_Key = size_t;
875 [[nodiscard]] State_Key state_key(const State &state) const noexcept
876 {
877 return state.node;
878 }
879};
880
882{
883 auto undo_called = std::make_shared<bool>(false);
887
888 // First call should throw and rollback.
889 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}, visited), std::runtime_error);
890 EXPECT_TRUE(*undo_called);
891 EXPECT_FALSE(visited.contains(0)); // Root key 0 should have been rolled back.
892
893 // Retry with same visited map to ensure it's clean.
894 *undo_called = false;
895 EXPECT_THROW((void) engine.search(ThrowingApplyState{undo_called, 0}, visited), std::runtime_error);
896 EXPECT_TRUE(*undo_called);
897 EXPECT_FALSE(visited.contains(0));
898}
899
901{
902 ArtificialMaxDomain domain;
904 policy.stop_at_first_solution = true;
905
906 auto result = branch_and_bound_search(domain, ArtificialState{}, policy);
907
908 ASSERT_TRUE(result.found_solution());
909 EXPECT_TRUE(result.stopped_on_solution());
910 EXPECT_EQ(result.stats.solutions_found, 1u);
911 EXPECT_EQ(result.incumbent.best_value(), 7);
912 EXPECT_EQ(artificial_signature(result.incumbent.get().path), "LL");
913}
914
916{
917 const Array<Knapsack_Item<int, double>> items = {
918 {2, 40.0}, {5, 30.0}, {10, 50.0}, {5, 10.0}
919 };
920 constexpr int capacity = 16;
921
922 KnapsackBBDomain domain(items, capacity, true);
923 auto result = branch_and_bound_search(domain, KnapsackState(items.size()));
924 const double optimum = knapsack_01_value<double, int>(items, capacity);
925
926 ASSERT_TRUE(result.found_solution());
927 EXPECT_DOUBLE_EQ(result.incumbent.best_value(), 90.0);
928 EXPECT_DOUBLE_EQ(result.incumbent.best_value(), optimum);
929}
930
932{
933 const Array<Knapsack_Item<int, double>> items = {
934 {2, 40.0}, {5, 30.0}, {10, 50.0}, {5, 10.0}
935 };
936 constexpr int capacity = 16;
937
938 KnapsackBBDomain domain(items, capacity, true);
940
942 policy.strategy = ExplorationPolicy::Strategy::Best_First;
944 auto best_result = best_first.search(KnapsackState(items.size()));
945
946 EXPECT_DOUBLE_EQ(depth_result.incumbent.best_value(), best_result.incumbent.best_value());
947 EXPECT_GT(best_result.stats.pruned_by_bound, 0u);
948}
949
951{
952 const Array<Knapsack_Item<int, double>> items = {
953 {10, 100.0}, {9, 80.0}, {9, 80.0}, {9, 80.0}
954 };
955 constexpr int capacity = 10;
956
957 auto tight_result = branch_and_bound_search(KnapsackBBDomain(items, capacity, true),
958 KnapsackState(items.size()));
959 auto weak_result = branch_and_bound_search(KnapsackBBDomain(items, capacity, false),
960 KnapsackState(items.size()));
961
962 EXPECT_DOUBLE_EQ(tight_result.incumbent.best_value(), weak_result.incumbent.best_value());
963 EXPECT_GT(tight_result.stats.pruned_by_bound, 0u);
964 EXPECT_LT(tight_result.stats.visited_states, weak_result.stats.visited_states);
965 EXPECT_LT(tight_result.stats.expanded_states, weak_result.stats.expanded_states);
966}
967
969{
970 const Array<Array<int>> costs = {
971 Array<int>{9, 2, 7, 8},
972 Array<int>{6, 4, 3, 7},
973 Array<int>{5, 8, 1, 8},
974 Array<int>{7, 6, 9, 4}
975 };
976
977 AssignmentBBDomain domain(costs);
979 domain,
980 Branch_And_Bound<AssignmentBBDomain, Minimize_Objective<int>>::default_policy(),
981 {},
983
984 auto result = engine.search(AssignmentState(costs.size()));
985
987 const int brute = brute_assignment_cost(costs, 0, used);
988
989 ASSERT_TRUE(result.found_solution());
990 EXPECT_EQ(result.incumbent.best_value(), brute);
991 EXPECT_EQ(result.incumbent.best_value(), 13);
992}
993
995{
996 const Array<Array<int>> costs = {
997 Array<int>{9, 2, 7, 8},
998 Array<int>{6, 4, 3, 7},
999 Array<int>{5, 8, 1, 8},
1000 Array<int>{7, 6, 9, 4}
1001 };
1002
1003 AssignmentBBDomain domain(costs);
1005 domain,
1006 Branch_And_Bound<AssignmentBBDomain, Minimize_Objective<int>>::default_policy(),
1007 {},
1009 auto depth_result = depth_engine.search(AssignmentState(costs.size()));
1010
1011 ExplorationPolicy policy = Branch_And_Bound<AssignmentBBDomain,
1012 Minimize_Objective<int>>::default_policy();
1013 policy.strategy = ExplorationPolicy::Strategy::Best_First;
1015 domain, policy, {}, Minimize_Objective<int>{});
1016 auto best_result = best_engine.search(AssignmentState(costs.size()));
1017
1018 EXPECT_EQ(depth_result.incumbent.best_value(), best_result.incumbent.best_value());
1019 EXPECT_GE(best_result.stats.pruned_by_bound, 0u);
1020}
1021
1022// ===========================================================================
1023// H3: B&B with max_depth limit, max_expansions limit, knapsack capacity=0
1024// ===========================================================================
1025
1026// max_depth=1: root is expanded (depth 0→1), but children at depth 1 are
1027// pruned before expansion. Since leaves are at depth 2, no complete solution
1028// is recorded.
1030{
1031 ArtificialMaxDomain domain;
1033 policy.stop_at_first_solution = false;
1034
1035 SearchLimits limits;
1036 limits.max_depth = 1;
1037
1038 Branch_And_Bound<ArtificialMaxDomain> engine(domain, policy, limits);
1039 auto result = engine.search(ArtificialState{});
1040
1041 EXPECT_FALSE(result.found_solution());
1042 EXPECT_GT(result.stats.pruned_by_depth, 0u);
1043}
1044
1045// max_expansions=1: only the root node is expanded. The two children are
1046// visited but not expanded, so no leaves are reached and no solution found.
1048{
1049 ArtificialMaxDomain domain;
1051 policy.stop_at_first_solution = false;
1052
1053 SearchLimits limits;
1054 limits.max_expansions = 1;
1055
1056 Branch_And_Bound<ArtificialMaxDomain> engine(domain, policy, limits);
1057 auto result = engine.search(ArtificialState{});
1058
1059 EXPECT_TRUE(result.limit_reached());
1060 EXPECT_EQ(result.stats.expanded_states, 1u);
1061 EXPECT_FALSE(result.found_solution());
1062}
1063
1064// Knapsack with capacity=0: no item fits, so the only complete solution is
1065// the empty selection with value=0.
1067{
1068 const Array<Knapsack_Item<int, double>> items = {
1069 {2, 40.0}, {5, 30.0}, {10, 50.0}
1070 };
1071 constexpr int capacity = 0;
1072
1073 KnapsackBBDomain domain(items, capacity, true);
1074 auto result = branch_and_bound_search(domain, KnapsackState(items.size()));
1075
1076 ASSERT_TRUE(result.found_solution());
1077 EXPECT_DOUBLE_EQ(result.incumbent.best_value(), 0.0);
1078}
1079
1080// ===========================================================================
1081// H4: IncrementalBoundProvider fast path in collect_ordered_moves
1082// ===========================================================================
1083
1085{
1086 IncrementalBoundDomain domain;
1087 ExplorationPolicy policy;
1088 policy.move_ordering = MoveOrderingMode::Estimated_Bound;
1089 policy.stop_at_first_solution = false;
1090
1092 auto result = engine.search(ArtificialState{});
1093
1094 // Same tree as ArtificialMaxDomain: optimal = 9.
1095 ASSERT_TRUE(result.found_solution());
1096 EXPECT_EQ(result.incumbent.best_value(), 9);
1097
1098 // bound_after must have been called (fast path taken).
1099 EXPECT_GT(engine.domain().bound_after_calls, 0u);
1100
1101 // Move ordering stats must reflect priority estimates.
1102 EXPECT_GT(result.stats.move_ordering.priority_estimates, 0u);
1103}
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:287
size_t size_t int32_t value
Definition ca-c-api.h:116
size_t size_t int32_t * out
Definition ca-c-api.h:120
size_t row
Definition ca-c-api.h:115
size_t size_t col
Definition ca-c-api.h:116
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:138
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
Definition tpl_array.H:365
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)
size_t blossom_maximum_cardinality_matching(const GT &g, DynDlist< typename GT::Arc * > &matching, SA sa=SA())
Alias of compute_maximum_cardinality_general_matching().
Definition Blossom.H:466
constexpr std::uint32_t delta
File contains only the cells that changed relative to a baseline.
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
size_t size(Node *root) noexcept
and
Check uniqueness with explicit hash + equality functors.
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:93
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