Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
state_search_framework_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 <cstdint>
32#include <memory>
33#include <stdexcept>
34#include <string>
35
36#include <gtest/gtest.h>
37
38#include <ah-errors.H>
39#include <State_Search.H>
41
42using namespace Aleph;
43
44namespace
45{
46
47struct ArtificialTreeState
48{
49 size_t depth = 0;
50 size_t code = 1;
51};
52
53struct ArtificialDecisionTreeDomain
54{
55 struct Move
56 {
57 char label = 'L';
58 };
59
60 using State = ArtificialTreeState;
61 using State_Key = std::uint64_t;
62
63 size_t leaf_depth = 2;
64
65 [[nodiscard]] State_Key state_key(const State &state) const noexcept
66 {
67 return (static_cast<State_Key>(state.depth) << 32)
68 ^ static_cast<State_Key>(state.code);
69 }
70
71 bool is_goal(const State &state) const
72 {
73 return state.depth == leaf_depth and (state.code == 5 or state.code == 6);
74 }
75
76 bool is_terminal(const State &state) const
77 {
78 return state.depth == leaf_depth;
79 }
80
81 void apply(State &state, const Move &move) const
82 {
83 state.code = state.code*2 + (move.label == 'R' ? 1u : 0u);
84 ++state.depth;
85 }
86
87 void undo(State &state, const Move &move) const
88 {
89 --state.depth;
90 state.code = (state.code - (move.label == 'R' ? 1u : 0u))/2;
91 }
92
93 template <typename Visitor>
94 bool for_each_successor(const State &state, Visitor visit) const
95 {
96 if (state.depth >= leaf_depth)
97 return true;
98
99 if (not visit(Move{'L'}))
100 return false;
101
102 return visit(Move{'R'});
103 }
104};
105
106struct NQueensState
107{
108 size_t n = 0;
109 size_t row = 0;
110 Array<int> queens;
111 Array<unsigned char> used_columns;
112 Array<unsigned char> used_diag_down;
113 Array<unsigned char> used_diag_up;
114
115 explicit NQueensState(const size_t size = 0)
116 : n(size),
117 row(0),
118 queens(size, -1),
119 used_columns(size, 0),
120 used_diag_down(size == 0 ? 0 : 2*size - 1, 0),
121 used_diag_up(size == 0 ? 0 : 2*size - 1, 0)
122 {
123 // empty
124 }
125};
126
127struct NQueensDomain
128{
129 struct Move
130 {
131 size_t col = 0;
132 };
133
134 using State = NQueensState;
135 using State_Key = std::uint64_t;
136
137 size_t n = 0;
138
139 [[nodiscard]] State_Key state_key(const State &state) const noexcept
140 {
141 State_Key key = static_cast<State_Key>(state.row);
142 for (size_t row = 0; row < state.n; ++row)
143 {
144 const int col = state.queens[row];
145 key = key * static_cast<State_Key>(1315423911u)
146 + static_cast<State_Key>(col + 2);
147 }
148
149 return key;
150 }
151
152 bool is_goal(const State &state) const
153 {
154 return state.row == n;
155 }
156
157 void apply(State &state, const Move &move) const
158 {
159 const size_t row = state.row;
160 const size_t down = row + state.n - 1 - move.col;
161 const size_t up = row + move.col;
162
163 state.queens[row] = static_cast<int>(move.col);
164 state.used_columns[move.col] = 1;
165 state.used_diag_down[down] = 1;
166 state.used_diag_up[up] = 1;
167 ++state.row;
168 }
169
170 void undo(State &state, const Move &move) const
171 {
172 --state.row;
173 const size_t row = state.row;
174 const size_t down = row + state.n - 1 - move.col;
175 const size_t up = row + move.col;
176
177 state.queens[row] = -1;
178 state.used_columns[move.col] = 0;
179 state.used_diag_down[down] = 0;
180 state.used_diag_up[up] = 0;
181 }
182
183 template <typename Visitor>
184 bool for_each_successor(const State &state, Visitor visit) const
185 {
186 if (state.row >= n)
187 return true;
188
189 for (size_t col = 0; col < n; ++col)
190 {
191 const size_t down = state.row + state.n - 1 - col;
192 const size_t up = state.row + col;
193 if (state.used_columns[col] or state.used_diag_down[down] or state.used_diag_up[up])
194 continue;
195
196 if (not visit(Move{col}))
197 return false;
198 }
199
200 return true;
201 }
202};
203
204struct SubsetSumState
205{
206 size_t index = 0;
207 int sum = 0;
209
210 explicit SubsetSumState(const size_t n = 0)
211 : index(0), sum(0), chosen(n, 0)
212 {
213 // empty
214 }
215};
216
217class SubsetSumDomain
218{
219public:
220 struct Move
221 {
222 bool take = false;
223 };
224
225 using State = SubsetSumState;
226 using State_Key = std::uint64_t;
227
228 explicit SubsetSumDomain(const Array<int> &values, const int target)
229 : values_(values),
230 suffix_remaining_(values.size() + 1, 0),
231 target_(target)
232 {
233 for (size_t i = values_.size(); i > 0; --i)
234 suffix_remaining_[i - 1] = suffix_remaining_[i] + values_[i - 1];
235 }
236
237 bool is_goal(const State &state) const
238 {
239 return state.sum == target_;
240 }
241
242 bool is_terminal(const State &state) const
243 {
244 return state.index == values_.size();
245 }
246
247 bool should_prune(const State &state, const size_t) const
248 {
249 return state.sum > target_ or state.sum + suffix_remaining_[state.index] < target_;
250 }
251
252 void apply(State &state, const Move &move) const
253 {
254 if (move.take)
255 state.sum += values_[state.index];
256
257 state.chosen[state.index] = move.take ? 1 : 0;
258 ++state.index;
259 }
260
261 void undo(State &state, const Move &move) const
262 {
263 --state.index;
264 if (move.take)
265 state.sum -= values_[state.index];
266
267 state.chosen[state.index] = 0;
268 }
269
270 template <typename Visitor>
271 bool for_each_successor(const State &state, Visitor visit) const
272 {
273 if (state.index >= values_.size())
274 return true;
275
276 if (not visit(Move{true}))
277 return false;
278
279 return visit(Move{false});
280 }
281
282 [[nodiscard]] State_Key state_key(const State &state) const noexcept
283 {
284 State_Key key = static_cast<State_Key>(state.index);
285 key = key * static_cast<State_Key>(2654435761u)
286 + static_cast<State_Key>(state.sum ^ 0x9e3779b9);
287 for (const auto pick : state.chosen)
288 key = (key << 1) ^ static_cast<State_Key>(pick);
289 return key;
290 }
291
292private:
293 Array<int> values_;
294 Array<int> suffix_remaining_;
295 int target_ = 0;
296};
297
298struct TinyIDAStarState
299{
300 size_t node = 0;
301 Array<size_t> history;
302};
303
304struct TinyIDAStarDomain
305{
306 struct Move
307 {
308 size_t to = 0;
309 int cost = 0;
310 };
311
312 using State = TinyIDAStarState;
313 using State_Key = std::uint64_t;
314 using Distance = int;
315
316 TinyIDAStarDomain()
317 : adjacency_{
318 Array<Move>{Move{1, 4}, Move{2, 1}},
319 Array<Move>{Move{4, 2}},
320 Array<Move>{Move{3, 1}},
321 Array<Move>{Move{4, 5}},
322 Array<Move>{}},
323 heuristic_{6, 2, 6, 5, 0}
324 {
325 // empty
326 }
327
328 [[nodiscard]] State_Key state_key(const State &state) const noexcept
329 {
330 return (static_cast<State_Key>(state.node) << 32) | static_cast<State_Key>(state.history.size());
331 }
332
333 [[nodiscard]] bool is_goal(const State &state) const noexcept
334 {
335 return state.node == goal_;
336 }
337
338 [[nodiscard]] bool is_terminal(const State &state) const noexcept
339 {
340 return adjacency_[state.node].is_empty();
341 }
342
343 void apply(State &state, const Move &move) const
344 {
345 state.history.append(state.node);
346 state.node = move.to;
347 }
348
349 void undo(State &state, const Move &) const
350 {
351 if (state.history.is_empty())
352 {
353 state.node = 0;
354 return;
355 }
356
357 const size_t previous = state.history[state.history.size() - 1];
358 (void) state.history.remove_last();
359 state.node = previous;
360 }
361
362 template <typename Visitor>
363 bool for_each_successor(const State &state, Visitor visit) const
364 {
365 for (const auto &move : adjacency_[state.node])
366 {
367 if (not visit(move))
368 return false;
369 }
370
371 return true;
372 }
373
374 [[nodiscard]] Distance heuristic(const State &state) const noexcept
375 {
376 return heuristic_[state.node];
377 }
378
379 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
380 {
381 return move.cost;
382 }
383
384private:
385 Array<Array<Move>> adjacency_;
386 Array<Distance> heuristic_;
387 size_t goal_ = 4;
388};
389
390struct ThrowingApplyIDAState
391{
392 std::shared_ptr<bool> undo_called;
393 size_t node = 0;
394};
395
396struct ThrowingApplyIDADomain
397{
398 struct Move
399 {
400 size_t to = 1;
401 int cost = 1;
402 };
403
404 using State = ThrowingApplyIDAState;
405 using State_Key = std::uint64_t;
406 using Distance = int;
407
408 [[nodiscard]] State_Key state_key(const State &state) const noexcept
409 {
410 return state.node;
411 }
412
413 [[nodiscard]] bool is_goal(const State &) const noexcept
414 {
415 return false;
416 }
417
418 [[nodiscard]] bool is_terminal(const State &state) const noexcept
419 {
420 return state.node != 0;
421 }
422
423 void apply(State &state, const Move &) const
424 {
425 (void) state;
426 ah_runtime_error() << "apply failed";
427 }
428
429 void undo(State &state, const Move &) const
430 {
431 *state.undo_called = true;
432 state.node = 0;
433 }
434
435 template <typename Visitor>
436 bool for_each_successor(const State &state, Visitor visit) const
437 {
438 if (state.node != 0)
439 return true;
440
441 return visit(Move{1, 1});
442 }
443
444 [[nodiscard]] Distance heuristic(const State &) const noexcept
445 {
446 return 0;
447 }
448
449 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
450 {
451 return move.cost;
452 }
453};
454
455struct ThrowingPostApplyIDAState
456{
457 std::shared_ptr<bool> undo_called;
458 size_t node = 0;
459};
460
461struct ThrowingPostApplyIDADomain
462{
463 struct Move
464 {
465 size_t to = 1;
466 int cost = 1;
467 };
468
469 using State = ThrowingPostApplyIDAState;
470 using State_Key = std::uint64_t;
471 using Distance = int;
472
473 [[nodiscard]] State_Key state_key(const State &state) const noexcept
474 {
475 return state.node;
476 }
477
478 [[nodiscard]] bool is_goal(const State &) const noexcept
479 {
480 return false;
481 }
482
483 [[nodiscard]] bool is_terminal(const State &state) const noexcept
484 {
485 return state.node != 0;
486 }
487
488 void apply(State &state, const Move &move) const
489 {
490 state.node = move.to;
491 }
492
493 void undo(State &state, const Move &) const
494 {
495 *state.undo_called = true;
496 state.node = 0;
497 }
498
499 template <typename Visitor>
500 bool for_each_successor(const State &state, Visitor visit) const
501 {
502 if (state.node != 0)
503 return true;
504
505 return visit(Move{1, 1});
506 }
507
508 [[nodiscard]] Distance heuristic(const State &state) const
509 {
510 if (state.node == 1)
511 ah_runtime_error() << "post-apply heuristic failed";
512 return 0;
513 }
514
515 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
516 {
517 return move.cost;
518 }
519};
520
525
529
534static_assert(DomainPruner<SubsetSumDomain>);
535
536// Moves live in SearchPath, an Aleph Array: without a default constructor or
537// copy assignment the engines used to fail inside tpl_memArray.H/tpl_array.H.
538struct NoDefaultMove
539{
540 int delta;
541 explicit NoDefaultMove(const int d) : delta(d) {}
542};
543
544struct NoCopyAssignMove
545{
546 int delta = 0;
547 NoCopyAssignMove() = default;
548 NoCopyAssignMove(const NoCopyAssignMove &) = default;
549 NoCopyAssignMove(NoCopyAssignMove &&) = default;
550 NoCopyAssignMove &operator=(NoCopyAssignMove &&) = default;
551 NoCopyAssignMove &operator=(const NoCopyAssignMove &) = delete;
552};
553
555static_assert(not ArrayStorable<NoDefaultMove>);
557
558// States are snapshotted by copy construction only (BestSolution assigns
559// through a temporary), so copy assignment is not required of them.
560struct NoCopyAssignState
561{
562 int depth = 0;
563 NoCopyAssignState() = default;
564 NoCopyAssignState(const NoCopyAssignState &) = default;
565 NoCopyAssignState(NoCopyAssignState &&) = default;
566 NoCopyAssignState &operator=(NoCopyAssignState &&) = default;
567 NoCopyAssignState &operator=(const NoCopyAssignState &) = delete;
568};
569
570static_assert(SearchState<NoCopyAssignState>);
571
575
576// H2: SearchStorageSet satisfies VisitedStateSet concept.
577static_assert(VisitedStateSet<SearchStorageSet<size_t>, size_t>);
578
579template <typename Move>
580std::string path_signature(const SearchPath<Move> &path)
581{
582 std::string signature;
583 for (const auto &move : path)
584 signature.push_back(move.label);
585 return signature;
586}
587
588std::string nqueens_signature(const NQueensState &state)
589{
590 std::string signature;
591 for (size_t row = 0; row < state.n; ++row)
592 signature.push_back(static_cast<char>('0' + state.queens[row]));
593 return signature;
594}
595
596std::string subset_sum_signature(const SubsetSumState &state)
597{
598 std::string signature;
599 for (const auto pick : state.chosen)
600 signature.push_back(pick ? '1' : '0');
601 return signature;
602}
603
605{
606 std::string signature;
607 size_t node = 0;
608 signature.push_back(static_cast<char>('0' + node));
609 for (const auto &move : path)
610 {
611 node = move.to;
612 signature.push_back(static_cast<char>('0' + node));
613 }
614 return signature;
615}
616
617template <typename SolutionList, typename Projector>
618bool contains_signature(const SolutionList &solutions,
619 const std::string &expected,
621{
622 for (auto it = solutions.get_it(); it.has_curr(); it.next_ne())
623 if (projector(it.get_curr()) == expected)
624 return true;
625
626 return false;
627}
628
629} // end namespace
630
632{
633 SearchStats stats;
634 SearchLimits limits;
635 ExplorationPolicy policy;
636
637 EXPECT_EQ(stats.visited_states, 0u);
638 EXPECT_EQ(stats.terminal_states, 0u);
639 EXPECT_EQ(stats.pruned_by_domain, 0u);
641 EXPECT_EQ(policy.strategy, ExplorationPolicy::Strategy::Depth_First);
643
644 BestSolution<int> incumbent;
645 EXPECT_FALSE(incumbent.has_value());
646 EXPECT_TRUE(incumbent.consider(7));
647 EXPECT_EQ(incumbent.get(), 7);
648 EXPECT_FALSE(incumbent.consider(9));
649
651 EXPECT_TRUE(collector.is_empty());
653 EXPECT_FALSE(collector.is_empty());
655 EXPECT_EQ(collector.size(), 2u);
656
657 SearchPath<int> path;
658 path.append(1);
659 path.append(2);
660 EXPECT_EQ(path.size(), 2u);
661
663 auto *entry = memo.insert(1, 11);
664 ASSERT_NE(entry, nullptr);
665 EXPECT_EQ(entry->first, 1);
666 EXPECT_EQ(entry->second, 11);
667}
668
670{
671 ArtificialDecisionTreeDomain domain;
673
674 auto result = engine.search(ArtificialTreeState{});
675
676 ASSERT_TRUE(result.found_solution());
677 EXPECT_TRUE(result.stopped_on_solution());
678 EXPECT_EQ(result.stats.visited_states, 4u);
679 EXPECT_EQ(result.stats.expanded_states, 2u);
680 EXPECT_EQ(result.stats.generated_successors, 3u);
681 EXPECT_EQ(result.stats.solutions_found, 1u);
682 EXPECT_EQ(result.stats.terminal_states, 1u);
683 EXPECT_EQ(result.stats.max_depth_reached, 2u);
684
685 EXPECT_EQ(path_signature(result.best_solution.get().path), "LR");
686}
687
689{
690 ArtificialDecisionTreeDomain domain;
691 ExplorationPolicy policy;
692 policy.stop_at_first_solution = false;
693
696
697 auto result = engine.search(ArtificialTreeState{}, collector);
698
699 EXPECT_TRUE(result.exhausted());
700 EXPECT_EQ(result.stats.visited_states, 7u);
701 EXPECT_EQ(result.stats.expanded_states, 3u);
702 EXPECT_EQ(result.stats.generated_successors, 6u);
703 EXPECT_EQ(result.stats.solutions_found, 2u);
704 EXPECT_EQ(result.stats.terminal_states, 2u);
705 EXPECT_EQ(collector.size(), 2u);
706 EXPECT_TRUE(contains_signature(collector.solutions(), "LR",
707 [](const auto &solution)
708 {
709 return path_signature(solution.path);
710 }));
711 EXPECT_TRUE(contains_signature(collector.solutions(), "RL",
712 [](const auto &solution)
713 {
714 return path_signature(solution.path);
715 }));
716}
717
719{
720 ArtificialDecisionTreeDomain domain;
721 ExplorationPolicy policy;
722 policy.stop_at_first_solution = false;
723
724 SearchLimits limits;
725 limits.max_depth = 1;
726
729
730 auto result = engine.search(ArtificialTreeState{}, collector);
731
732 EXPECT_FALSE(result.found_solution());
733 EXPECT_TRUE(result.exhausted());
734 EXPECT_EQ(result.stats.visited_states, 3u);
735 EXPECT_EQ(result.stats.expanded_states, 1u);
736 EXPECT_EQ(result.stats.pruned_by_depth, 2u);
737 EXPECT_EQ(collector.size(), 0u);
738}
739
741{
742 NQueensDomain domain{4};
743 ExplorationPolicy policy;
744 policy.stop_at_first_solution = false;
745
748
749 auto result = engine.search(NQueensState(4), collector);
750
751 ASSERT_TRUE(result.found_solution());
752 EXPECT_TRUE(result.exhausted());
753 EXPECT_EQ(result.stats.solutions_found, 2u);
754 EXPECT_EQ(collector.size(), 2u);
755 EXPECT_EQ(result.best_solution.get().depth, 4u);
756 EXPECT_TRUE(contains_signature(collector.solutions(), "1302",
757 [](const auto &solution)
758 {
759 return nqueens_signature(solution.state);
760 }));
761 EXPECT_TRUE(contains_signature(collector.solutions(), "2031",
762 [](const auto &solution)
763 {
764 return nqueens_signature(solution.state);
765 }));
766}
767
769{
770 NQueensDomain domain{4};
771 ExplorationPolicy policy;
772 policy.stop_at_first_solution = false;
773
774 SearchLimits limits;
775 limits.max_depth = 3;
776
777 Depth_First_Backtracking<NQueensDomain> engine(domain, policy, limits);
778 auto result = engine.search(NQueensState(4));
779
780 EXPECT_FALSE(result.found_solution());
781 EXPECT_TRUE(result.exhausted());
782 EXPECT_GT(result.stats.pruned_by_depth, 0u);
783}
784
786{
787 SubsetSumDomain domain(Array<int>{4, 1, 1, 2}, 2);
788 ExplorationPolicy policy;
789 policy.stop_at_first_solution = false;
790
793
794 auto result = engine.search(SubsetSumState(4), collector);
795
796 ASSERT_TRUE(result.found_solution());
797 EXPECT_TRUE(result.exhausted());
798 EXPECT_EQ(result.stats.solutions_found, 2u);
799 EXPECT_EQ(collector.size(), 2u);
800 EXPECT_GT(result.stats.pruned_by_domain, 0u);
801 EXPECT_TRUE(contains_signature(collector.solutions(), "0110",
802 [](const auto &solution)
803 {
804 return subset_sum_signature(solution.state);
805 }));
806 EXPECT_TRUE(contains_signature(collector.solutions(), "0001",
807 [](const auto &solution)
808 {
809 return subset_sum_signature(solution.state);
810 }));
811}
812
814{
815 SubsetSumDomain domain(Array<int>{4, 1, 1, 2}, 2);
816 ExplorationPolicy policy;
817 policy.stop_at_first_solution = false;
818
819 SearchLimits limits;
820 limits.max_solutions = 1;
821
824
825 auto result = engine.search(SubsetSumState(4), collector);
826
827 EXPECT_TRUE(result.limit_reached());
828 EXPECT_EQ(result.stats.solutions_found, 1u);
829 EXPECT_EQ(collector.size(), 1u);
830}
831
833{
834 TinyIDAStarDomain domain;
836
837 auto result = engine.search(TinyIDAStarDomain::State{});
838
839 ASSERT_TRUE(result.found_solution());
840 EXPECT_EQ(result.total_cost, 6);
841 ASSERT_TRUE(result.best_solution.has_value());
842 EXPECT_EQ(result.best_solution.get().path.size(), 2u);
843 EXPECT_EQ(ida_star_path_signature(result.best_solution.get().path), "014");
844 ASSERT_FALSE(result.iterations.is_empty());
845 EXPECT_EQ(result.iterations[0].threshold, 6);
846}
847
849{
850 TinyIDAStarDomain domain;
851 SearchLimits limits;
852 limits.max_depth = 1;
853
855 auto result = engine.search(TinyIDAStarDomain::State{});
856
857 EXPECT_FALSE(result.found_solution());
858 EXPECT_TRUE(result.exhausted());
859 EXPECT_GT(result.stats.pruned_by_depth, 0u);
860}
861
863{
864 TinyIDAStarDomain domain;
865 ExplorationPolicy policy;
866 policy.stop_at_first_solution = false;
867
868 size_t callbacks = 0;
869 auto on_solution = [&](const auto &solution) {
870 (void) solution;
871 ++callbacks;
872 return callbacks == 1 ? false : true;
873 };
874
876 auto result = engine.search(TinyIDAStarDomain::State{}, on_solution);
877
878 EXPECT_TRUE(result.stopped_on_solution());
879 EXPECT_EQ(callbacks, 1u);
880 EXPECT_EQ(result.stats.solutions_found, 1u);
881}
882
884{
885 auto undo_called = std::make_shared<bool>(false);
886 ThrowingApplyIDADomain domain;
888
889 EXPECT_THROW((void) engine.search(ThrowingApplyIDAState{undo_called, 0}), std::runtime_error);
890 EXPECT_FALSE(*undo_called);
891}
892
894{
895 auto undo_called = std::make_shared<bool>(false);
896 ThrowingPostApplyIDADomain domain;
898
899 EXPECT_THROW((void) engine.search(ThrowingPostApplyIDAState{undo_called, 0}), std::runtime_error);
900 EXPECT_TRUE(*undo_called);
901}
902
903// ---------------------------------------------------------------------------
904// Edge-case tests (Recommendation 5)
905// ---------------------------------------------------------------------------
906
907// max_depth=0: root is visited but never expanded — no solutions possible
908// in a tree where goals live at depth 2.
910{
911 ArtificialDecisionTreeDomain domain;
912 ExplorationPolicy policy;
913 policy.stop_at_first_solution = false;
914
915 SearchLimits limits;
916 limits.max_depth = 0;
917
919 auto result = engine.search(ArtificialTreeState{});
920
921 EXPECT_FALSE(result.found_solution());
922 EXPECT_TRUE(result.exhausted());
923 EXPECT_EQ(result.stats.visited_states, 1u);
924 EXPECT_EQ(result.stats.expanded_states, 0u);
925 EXPECT_EQ(result.stats.pruned_by_depth, 1u);
926 EXPECT_EQ(result.stats.generated_successors, 0u);
927}
928
929// max_expansions=1: only the root is expanded — its two children are visited
930// but never expanded themselves, so no solutions at depth 2.
932{
933 ArtificialDecisionTreeDomain domain;
934 ExplorationPolicy policy;
935 policy.stop_at_first_solution = false;
936
937 SearchLimits limits;
938 limits.max_expansions = 1;
939
941 auto result = engine.search(ArtificialTreeState{});
942
943 EXPECT_TRUE(result.limit_reached());
944 EXPECT_EQ(result.stats.expanded_states, 1u);
945 EXPECT_GE(result.stats.generated_successors, 1u);
946 EXPECT_LE(result.stats.generated_successors, 2u);
947 EXPECT_EQ(result.stats.solutions_found, 0u);
948}
949
950// Root is goal: a domain where the initial state already satisfies is_goal().
951// The engine must find a solution with an empty path at depth 0.
952namespace
953{
954
955struct RootGoalDomain
956{
957 struct Move
958 {
959 int id = 0;
960 };
961
962 using State = ArtificialTreeState;
963 using State_Key = size_t;
964
965 [[nodiscard]] State_Key state_key(const State &state) const noexcept
966 {
967 return state.code;
968 }
969
970 bool is_goal(const State &) const
971 {
972 return true;
973 }
974
975 void apply(State &state, const Move &) const
976 {
977 ++state.depth;
978 }
979
980 void undo(State &state, const Move &) const
981 {
982 --state.depth;
983 }
984
985 template <typename Visitor>
986 bool for_each_successor(const State &, Visitor visit) const
987 {
988 return visit(Move{1});
989 }
990};
991
992} // end namespace
993
995{
996 RootGoalDomain domain;
998
999 auto result = engine.search(ArtificialTreeState{});
1000
1001 ASSERT_TRUE(result.found_solution());
1002 EXPECT_TRUE(result.stopped_on_solution());
1003 EXPECT_EQ(result.stats.solutions_found, 1u);
1004 EXPECT_EQ(result.stats.visited_states, 1u);
1005 EXPECT_EQ(result.stats.expanded_states, 0u);
1006 EXPECT_EQ(result.best_solution.get().depth, 0u);
1007 EXPECT_TRUE(result.best_solution.get().path.is_empty());
1008}
1009
1010// Root is goal with max_depth=0: even at depth 0, the root is checked for
1011// goal before the depth limit prunes expansion — it should still find a solution.
1013{
1014 RootGoalDomain domain;
1015 SearchLimits limits;
1016 limits.max_depth = 0;
1017
1019
1020 auto result = engine.search(ArtificialTreeState{});
1021
1022 ASSERT_TRUE(result.found_solution());
1023 EXPECT_EQ(result.stats.solutions_found, 1u);
1024 EXPECT_EQ(result.best_solution.get().depth, 0u);
1025}
1026
1027// Cycles in the state space: a directed graph with cycles that would cause
1028// infinite recursion without a visited set. The graph is:
1029//
1030// 0 → 1 → 2 → 3 (goal)
1031// ↘ 0 (cycle back)
1032//
1033// Without visited-set, the cycle 0→1→0→1→... causes infinite recursion.
1034// With visited-set (search_visited), the engine detects the repeated state
1035// and finds the goal via the non-cyclic path.
1036namespace
1037{
1038
1039struct CyclicGraphState
1040{
1041 size_t node = 0;
1042};
1043
1044struct CyclicGraphDomain
1045{
1046 struct Move
1047 {
1048 size_t from = 0;
1049 size_t to = 0;
1050 };
1051
1052 using State = CyclicGraphState;
1053 using State_Key = size_t;
1054
1055 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1056 {
1057 return state.node;
1058 }
1059
1060 bool is_goal(const State &state) const
1061 {
1062 return state.node == 3;
1063 }
1064
1065 void apply(State &state, const Move &move) const
1066 {
1067 state.node = move.to;
1068 }
1069
1070 void undo(State &state, const Move &move) const
1071 {
1072 state.node = move.from;
1073 }
1074
1075 template <typename Visitor>
1076 bool for_each_successor(const State &state, Visitor visit) const
1077 {
1078 switch (state.node)
1079 {
1080 case 0:
1081 return visit(Move{0, 1});
1082 case 1:
1083 if (not visit(Move{1, 0})) // cycle back to 0 (tried first)
1084 return false;
1085 return visit(Move{1, 2});
1086 case 2:
1087 return visit(Move{2, 3});
1088 default:
1089 return true;
1090 }
1091 }
1092};
1093
1094} // end namespace
1095
1097{
1098 CyclicGraphDomain domain;
1100
1102 auto result = engine.search(CyclicGraphState{}, visited);
1103
1104 ASSERT_TRUE(result.found_solution());
1105 EXPECT_EQ(result.best_solution.get().state.node, 3u);
1106 EXPECT_GT(result.stats.pruned_by_visited, 0u);
1107}
1108
1109// Cycle with multiple paths of different depth: state S is reachable via a
1110// short path (depth 1) and a long path (depth 3). With the depth-aware
1111// visited map, the shorter path records depth 1 for S. When the longer path
1112// reaches S at depth 3, it should be pruned. Goal G is reachable only
1113// through S.
1114//
1115// 0 → S → G (short: depth 2)
1116// 0 → A → B → S → G (long: depth 4, pruned at S)
1117//
1118namespace
1119{
1120
1121struct MultiPathState
1122{
1123 int node = 0;
1124};
1125
1126struct MultiPathDomain
1127{
1128 struct Move
1129 {
1130 int from = 0;
1131 int to = 0;
1132 };
1133
1134 using State = MultiPathState;
1135 using State_Key = int;
1136
1137 static constexpr int S = 1;
1138 static constexpr int G = 2;
1139 static constexpr int A = 3;
1140 static constexpr int B = 4;
1141
1142 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1143 {
1144 return state.node;
1145 }
1146
1147 bool is_goal(const State &state) const
1148 {
1149 return state.node == G;
1150 }
1151
1152 void apply(State &state, const Move &move) const
1153 {
1154 state.node = move.to;
1155 }
1156
1157 void undo(State &state, const Move &move) const
1158 {
1159 state.node = move.from;
1160 }
1161
1162 template <typename Visitor>
1163 bool for_each_successor(const State &state, Visitor visit) const
1164 {
1165 switch (state.node)
1166 {
1167 case 0:
1168 if (not visit(Move{0, S})) // short path: 0 → S
1169 return false;
1170 return visit(Move{0, A}); // long path: 0 → A → B → S
1171 case S:
1172 return visit(Move{S, G});
1173 case A:
1174 return visit(Move{A, B});
1175 case B:
1176 return visit(Move{B, S}); // reaches S at depth 3 (pruned)
1177 default:
1178 return true;
1179 }
1180 }
1181};
1182
1183} // end namespace
1184
1186{
1187 MultiPathDomain domain;
1188 ExplorationPolicy policy;
1189 policy.stop_at_first_solution = false;
1190
1192
1194 auto result = engine.search(MultiPathState{}, visited);
1195
1196 ASSERT_TRUE(result.found_solution());
1197 EXPECT_EQ(result.stats.solutions_found, 1u);
1198 EXPECT_EQ(result.stats.pruned_by_visited, 1u);
1199 EXPECT_EQ(result.best_solution.get().state.node, MultiPathDomain::G);
1200}
1201
1202// IDA* with max_depth=0: root is visited but not expanded.
1204{
1205 TinyIDAStarDomain domain;
1206 SearchLimits limits;
1207 limits.max_depth = 0;
1208
1210 auto result = engine.search(TinyIDAStarDomain::State{});
1211
1212 EXPECT_FALSE(result.found_solution());
1213 EXPECT_TRUE(result.exhausted());
1214 EXPECT_GT(result.stats.pruned_by_depth, 0u);
1215 EXPECT_EQ(result.stats.expanded_states, 0u);
1216}
1217
1218// IDA* with root as goal: the initial state has node==goal.
1219namespace
1220{
1221
1222struct IDAStarRootGoalDomain
1223{
1224 struct Move
1225 {
1226 int to = 0;
1227 int cost = 1;
1228 };
1229
1230 using State = TinyIDAStarState;
1231 using State_Key = size_t;
1232 using Distance = int;
1233
1234 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1235 {
1236 return state.node;
1237 }
1238
1239 bool is_goal(const State &state) const
1240 {
1241 return state.node == 0;
1242 }
1243
1244 bool is_terminal(const State &) const
1245 {
1246 return false;
1247 }
1248
1249 void apply(State &state, const Move &move) const
1250 {
1251 state.history.append(state.node);
1252 state.node = static_cast<size_t>(move.to);
1253 }
1254
1255 void undo(State &state, const Move &) const
1256 {
1257 if (state.history.is_empty())
1258 return;
1259
1260 const size_t prev = state.history[state.history.size() - 1];
1261 (void) state.history.remove_last();
1262 state.node = prev;
1263 }
1264
1265 template <typename Visitor>
1266 bool for_each_successor(const State &, Visitor visit) const
1267 {
1268 return visit(Move{1, 5});
1269 }
1270
1271 [[nodiscard]] Distance heuristic(const State &) const noexcept
1272 {
1273 return 0;
1274 }
1275
1276 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1277 {
1278 return move.cost;
1279 }
1280};
1281
1282} // end namespace
1283
1285{
1286 IDAStarRootGoalDomain domain;
1288
1289 auto result = engine.search(TinyIDAStarState{});
1290
1291 ASSERT_TRUE(result.found_solution());
1292 EXPECT_EQ(result.total_cost, 0);
1293 EXPECT_EQ(result.stats.solutions_found, 1u);
1294 EXPECT_TRUE(result.best_solution.get().path.is_empty());
1295}
1296
1297// IDA* with zero-cost edges: all edges cost 0. The heuristic is 0 everywhere.
1298// This means f = g + h = 0 for all states and the initial threshold suffices.
1299namespace
1300{
1301
1302struct ZeroCostIDADomain
1303{
1304 struct Move
1305 {
1306 size_t to = 0;
1307 int cost = 0;
1308 };
1309
1310 using State = TinyIDAStarState;
1311 using State_Key = size_t;
1312 using Distance = int;
1313
1314 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1315 {
1316 return state.node;
1317 }
1318
1319 bool is_goal(const State &state) const
1320 {
1321 return state.node == 2;
1322 }
1323
1324 void apply(State &state, const Move &move) const
1325 {
1326 state.history.append(state.node);
1327 state.node = move.to;
1328 }
1329
1330 void undo(State &state, const Move &) const
1331 {
1332 if (state.history.is_empty())
1333 return;
1334
1335 const size_t prev = state.history[state.history.size() - 1];
1336 (void) state.history.remove_last();
1337 state.node = prev;
1338 }
1339
1340 template <typename Visitor>
1341 bool for_each_successor(const State &state, Visitor visit) const
1342 {
1343 if (state.node == 0)
1344 return visit(Move{1, 0});
1345 if (state.node == 1)
1346 return visit(Move{2, 0});
1347 return true;
1348 }
1349
1350 [[nodiscard]] Distance heuristic(const State &) const noexcept
1351 {
1352 return 0;
1353 }
1354
1355 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1356 {
1357 return move.cost;
1358 }
1359};
1360
1361} // end namespace
1362
1364{
1365 ZeroCostIDADomain domain;
1367
1368 auto result = engine.search(TinyIDAStarState{});
1369
1370 ASSERT_TRUE(result.found_solution());
1371 EXPECT_EQ(result.total_cost, 0);
1372 EXPECT_EQ(result.stats.solutions_found, 1u);
1373}
1374
1375// NQueens n=1: trivial single-queen problem — should find exactly 1 solution.
1377{
1378 NQueensDomain domain{0};
1379 ExplorationPolicy policy;
1380 policy.stop_at_first_solution = false;
1381
1384
1385 auto result = engine.search(NQueensState(0), collector);
1386
1387 ASSERT_TRUE(result.found_solution());
1388 EXPECT_TRUE(result.exhausted());
1389 EXPECT_EQ(result.stats.solutions_found, 1u);
1390 EXPECT_EQ(collector.size(), 1u);
1391 EXPECT_EQ(nqueens_signature(result.best_solution.get().state), "");
1392}
1393
1394// NQueens n=1: trivial single-queen problem — should find exactly 1 solution.
1396{
1397 NQueensDomain domain{1};
1398 ExplorationPolicy policy;
1399 policy.stop_at_first_solution = false;
1400
1403
1404 auto result = engine.search(NQueensState(1), collector);
1405
1406 ASSERT_TRUE(result.found_solution());
1407 EXPECT_TRUE(result.exhausted());
1408 EXPECT_EQ(result.stats.solutions_found, 1u);
1409 EXPECT_EQ(collector.size(), 1u);
1410 EXPECT_EQ(nqueens_signature(result.best_solution.get().state), "0");
1411}
1412
1413// NQueens n=8: realistic problem — should find all 92 solutions.
1415{
1416 NQueensDomain domain{8};
1417 ExplorationPolicy policy;
1418 policy.stop_at_first_solution = false;
1419
1422
1423 auto result = engine.search(NQueensState(8), collector);
1424
1425 ASSERT_TRUE(result.found_solution());
1426 EXPECT_TRUE(result.exhausted());
1427 EXPECT_EQ(result.stats.solutions_found, 92u);
1428 EXPECT_EQ(collector.size(), 92u);
1429}
1430
1431// H2: SearchStorageSet runtime sanity — basic contains/insert contract.
1433{
1435
1436 EXPECT_FALSE(visited.contains(1u));
1437 visited.insert(1u);
1438 EXPECT_TRUE(visited.contains(1u));
1439 EXPECT_FALSE(visited.contains(2u));
1440
1441 // Inserting the same key twice is a no-op (no duplicate).
1442 visited.insert(1u);
1443 EXPECT_TRUE(visited.contains(1u));
1444}
1445
1446// H5a: IDA* on a graph with no path to the goal terminates with Exhausted.
1447//
1448// Graph: 0 → 1 (only node 0 has a successor; goal is unreachable node 9)
1449namespace
1450{
1451
1452struct NoPathIDADomain
1453{
1454 struct Move
1455 {
1456 size_t to = 0;
1457 double cost = 1.0;
1458 };
1459
1460 using State = TinyIDAStarState;
1461 using State_Key = size_t;
1462 using Distance = double;
1463
1464 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1465 {
1466 return state.node;
1467 }
1468
1469 [[nodiscard]] bool is_goal(const State &state) const noexcept
1470 {
1471 return state.node == 9; // unreachable
1472 }
1473
1474 void apply(State &state, const Move &move) const
1475 {
1476 state.history.append(state.node);
1477 state.node = move.to;
1478 }
1479
1480 void undo(State &state, const Move &) const
1481 {
1482 if (state.history.is_empty())
1483 return;
1484 const size_t prev = state.history[state.history.size() - 1];
1485 (void) state.history.remove_last();
1486 state.node = prev;
1487 }
1488
1489 template <typename Visitor>
1490 bool for_each_successor(const State &state, Visitor visit) const
1491 {
1492 // Only node 0 has a successor; node 1 is a dead end.
1493 if (state.node == 0)
1494 return visit(Move{1, 1});
1495 return true;
1496 }
1497
1498 [[nodiscard]] Distance heuristic(const State &) const noexcept
1499 {
1500 return 1.0; // admissible: never overestimates for unreachable goal
1501 }
1502
1503 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1504 {
1505 return move.cost;
1506 }
1507};
1508
1509// H5b: domain with an inadmissible heuristic (overestimates).
1510// The IDA* engine still terminates and finds a solution, but the returned
1511// cost may be higher than the true optimum because the overestimating
1512// heuristic prunes the optimal path.
1513//
1514// Graph: 0 --1--> 1 --1--> 2 (goal) optimal = 2
1515// Heuristic: h(0)=100, h(1)=100, h(2)=0 (overestimates massively)
1516// With inadmissible h, the initial threshold = 100, so the first pass
1517// already visits all states and finds the solution at cost 2.
1518struct InadmissibleHeuristicDomain
1519{
1520 struct Move
1521 {
1522 size_t to = 0;
1523 double cost = 1.0;
1524 };
1525
1526 using State = TinyIDAStarState;
1527 using State_Key = size_t;
1528 using Distance = double;
1529
1530 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1531 {
1532 return state.node;
1533 }
1534
1535 [[nodiscard]] bool is_goal(const State &state) const noexcept
1536 {
1537 return state.node == 2;
1538 }
1539
1540 void apply(State &state, const Move &move) const
1541 {
1542 state.history.append(state.node);
1543 state.node = move.to;
1544 }
1545
1546 void undo(State &state, const Move &) const
1547 {
1548 if (state.history.is_empty())
1549 return;
1550 const size_t prev = state.history[state.history.size() - 1];
1551 (void) state.history.remove_last();
1552 state.node = prev;
1553 }
1554
1555 template <typename Visitor>
1556 bool for_each_successor(const State &state, Visitor visit) const
1557 {
1558 if (state.node == 0)
1559 return visit(Move{1, 1});
1560 if (state.node == 1)
1561 return visit(Move{2, 1});
1562 return true;
1563 }
1564
1565 [[nodiscard]] Distance heuristic(const State &state) const noexcept
1566 {
1567 // Massively inadmissible for non-goal nodes.
1568 return state.node == 2 ? 0.0 : 100.0;
1569 }
1570
1571 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1572 {
1573 return move.cost;
1574 }
1575};
1576
1577} // end namespace
1578
1580{
1581 NoPathIDADomain domain;
1583
1584 auto result = engine.search(TinyIDAStarState{});
1585
1586 EXPECT_FALSE(result.found_solution());
1587 EXPECT_TRUE(result.exhausted());
1588 ASSERT_FALSE(result.iterations.is_empty());
1589 EXPECT_EQ(result.iterations[result.iterations.size() - 1].next_threshold,
1590 ida_star_detail::distance_unreachable<double>());
1591}
1592
1594{
1595 InadmissibleHeuristicDomain domain;
1597
1598 auto result = engine.search(TinyIDAStarState{});
1599
1600 // With an inadmissible heuristic IDA* is no longer guaranteed optimal,
1601 // but it must still find a solution if one exists.
1602 ASSERT_TRUE(result.found_solution());
1603 EXPECT_DOUBLE_EQ(result.total_cost, 2.0);
1604}
Umbrella header for the implicit state-space search framework.
IDA* (Iterative Deepening A*) over implicit state spaces.
Exception handling system with formatted messages for Aleph-w.
#define ah_runtime_error()
Throws std::runtime_error unconditionally.
Definition ah-errors.H:287
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
constexpr bool is_empty() const noexcept
Checks if the container is empty.
Definition tpl_array.H:359
T & append(const T &data)
Append a copy of data
Definition tpl_array.H:250
Tracks the best solution seen so far according to a comparator.
bool has_value() const noexcept
Return true if an incumbent exists.
bool consider(const Solution &candidate)
Consider a candidate by copy.
const Solution & get() const
Read the current incumbent.
Recursive depth-first backtracking over an implicit state space.
Result search(State initial_state)
Execute a depth-first backtracking search from initial_state.
IDA* engine for implicit state spaces with an admissible heuristic.
Result search(State initial_state)
Run IDA* from initial_state.
Graph implemented with double-linked adjacency lists.
Definition tpl_graph.H:429
Collector that stores accepted solutions in an Aleph list.
Minimal std::expected-style result type for C++20.
#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.
@ S
Susceptible.
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
size_t size(Node *root) noexcept
constexpr size_t Search_Unlimited
Sentinel used by SearchLimits to mean "no bound".
and
Check uniqueness with explicit hash + equality functors.
std::string code(Node *root)
Compute a string with the Lukasiewicz`s word of a tree.
T sum(const Container &container, const T &init=T{})
Compute sum of all elements.
Exploration controls shared across engines.
bool stop_at_first_solution
Stop when the first goal is found.
Strategy strategy
Traversal strategy.
Hard bounds applied by the search engine.
size_t max_expansions
Maximum expanded states.
size_t max_solutions
Maximum accepted solutions.
size_t max_depth
Maximum expansion depth.
Counters collected during a search run.
size_t pruned_by_domain
States discarded by domain-side pruning.
size_t terminal_states
Non-solution terminal states cut by the domain.
size_t visited_states
Number of states entered by the engine.
void apply(State &s, const Move &m) const noexcept
State_Key state_key(const State &s) const noexcept
bool for_each_successor(const State &s, Visitor visit) const
void undo(State &s, const Move &m) const noexcept
bool is_goal(const State &s) const noexcept
Distance accessor.
static mt19937 engine