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
539
540// H2: SearchStorageSet satisfies VisitedStateSet concept.
541static_assert(VisitedStateSet<SearchStorageSet<size_t>, size_t>);
542
543template <typename Move>
544std::string path_signature(const SearchPath<Move> &path)
545{
546 std::string signature;
547 for (const auto &move : path)
548 signature.push_back(move.label);
549 return signature;
550}
551
552std::string nqueens_signature(const NQueensState &state)
553{
554 std::string signature;
555 for (size_t row = 0; row < state.n; ++row)
556 signature.push_back(static_cast<char>('0' + state.queens[row]));
557 return signature;
558}
559
560std::string subset_sum_signature(const SubsetSumState &state)
561{
562 std::string signature;
563 for (const auto pick : state.chosen)
564 signature.push_back(pick ? '1' : '0');
565 return signature;
566}
567
569{
570 std::string signature;
571 size_t node = 0;
572 signature.push_back(static_cast<char>('0' + node));
573 for (const auto &move : path)
574 {
575 node = move.to;
576 signature.push_back(static_cast<char>('0' + node));
577 }
578 return signature;
579}
580
581template <typename SolutionList, typename Projector>
582bool contains_signature(const SolutionList &solutions,
583 const std::string &expected,
585{
586 for (auto it = solutions.get_it(); it.has_curr(); it.next_ne())
587 if (projector(it.get_curr()) == expected)
588 return true;
589
590 return false;
591}
592
593} // end namespace
594
596{
597 SearchStats stats;
598 SearchLimits limits;
599 ExplorationPolicy policy;
600
601 EXPECT_EQ(stats.visited_states, 0u);
602 EXPECT_EQ(stats.terminal_states, 0u);
603 EXPECT_EQ(stats.pruned_by_domain, 0u);
605 EXPECT_EQ(policy.strategy, ExplorationPolicy::Strategy::Depth_First);
607
608 BestSolution<int> incumbent;
609 EXPECT_FALSE(incumbent.has_value());
610 EXPECT_TRUE(incumbent.consider(7));
611 EXPECT_EQ(incumbent.get(), 7);
612 EXPECT_FALSE(incumbent.consider(9));
613
615 EXPECT_TRUE(collector.is_empty());
617 EXPECT_FALSE(collector.is_empty());
619 EXPECT_EQ(collector.size(), 2u);
620
621 SearchPath<int> path;
622 path.append(1);
623 path.append(2);
624 EXPECT_EQ(path.size(), 2u);
625
627 auto *entry = memo.insert(1, 11);
628 ASSERT_NE(entry, nullptr);
629 EXPECT_EQ(entry->first, 1);
630 EXPECT_EQ(entry->second, 11);
631}
632
634{
635 ArtificialDecisionTreeDomain domain;
637
638 auto result = engine.search(ArtificialTreeState{});
639
640 ASSERT_TRUE(result.found_solution());
641 EXPECT_TRUE(result.stopped_on_solution());
642 EXPECT_EQ(result.stats.visited_states, 4u);
643 EXPECT_EQ(result.stats.expanded_states, 2u);
644 EXPECT_EQ(result.stats.generated_successors, 3u);
645 EXPECT_EQ(result.stats.solutions_found, 1u);
646 EXPECT_EQ(result.stats.terminal_states, 1u);
647 EXPECT_EQ(result.stats.max_depth_reached, 2u);
648
649 EXPECT_EQ(path_signature(result.best_solution.get().path), "LR");
650}
651
653{
654 ArtificialDecisionTreeDomain domain;
655 ExplorationPolicy policy;
656 policy.stop_at_first_solution = false;
657
660
661 auto result = engine.search(ArtificialTreeState{}, collector);
662
663 EXPECT_TRUE(result.exhausted());
664 EXPECT_EQ(result.stats.visited_states, 7u);
665 EXPECT_EQ(result.stats.expanded_states, 3u);
666 EXPECT_EQ(result.stats.generated_successors, 6u);
667 EXPECT_EQ(result.stats.solutions_found, 2u);
668 EXPECT_EQ(result.stats.terminal_states, 2u);
669 EXPECT_EQ(collector.size(), 2u);
670 EXPECT_TRUE(contains_signature(collector.solutions(), "LR",
671 [](const auto &solution)
672 {
673 return path_signature(solution.path);
674 }));
675 EXPECT_TRUE(contains_signature(collector.solutions(), "RL",
676 [](const auto &solution)
677 {
678 return path_signature(solution.path);
679 }));
680}
681
683{
684 ArtificialDecisionTreeDomain domain;
685 ExplorationPolicy policy;
686 policy.stop_at_first_solution = false;
687
688 SearchLimits limits;
689 limits.max_depth = 1;
690
693
694 auto result = engine.search(ArtificialTreeState{}, collector);
695
696 EXPECT_FALSE(result.found_solution());
697 EXPECT_TRUE(result.exhausted());
698 EXPECT_EQ(result.stats.visited_states, 3u);
699 EXPECT_EQ(result.stats.expanded_states, 1u);
700 EXPECT_EQ(result.stats.pruned_by_depth, 2u);
701 EXPECT_EQ(collector.size(), 0u);
702}
703
705{
706 NQueensDomain domain{4};
707 ExplorationPolicy policy;
708 policy.stop_at_first_solution = false;
709
712
713 auto result = engine.search(NQueensState(4), collector);
714
715 ASSERT_TRUE(result.found_solution());
716 EXPECT_TRUE(result.exhausted());
717 EXPECT_EQ(result.stats.solutions_found, 2u);
718 EXPECT_EQ(collector.size(), 2u);
719 EXPECT_EQ(result.best_solution.get().depth, 4u);
720 EXPECT_TRUE(contains_signature(collector.solutions(), "1302",
721 [](const auto &solution)
722 {
723 return nqueens_signature(solution.state);
724 }));
725 EXPECT_TRUE(contains_signature(collector.solutions(), "2031",
726 [](const auto &solution)
727 {
728 return nqueens_signature(solution.state);
729 }));
730}
731
733{
734 NQueensDomain domain{4};
735 ExplorationPolicy policy;
736 policy.stop_at_first_solution = false;
737
738 SearchLimits limits;
739 limits.max_depth = 3;
740
741 Depth_First_Backtracking<NQueensDomain> engine(domain, policy, limits);
742 auto result = engine.search(NQueensState(4));
743
744 EXPECT_FALSE(result.found_solution());
745 EXPECT_TRUE(result.exhausted());
746 EXPECT_GT(result.stats.pruned_by_depth, 0u);
747}
748
750{
751 SubsetSumDomain domain(Array<int>{4, 1, 1, 2}, 2);
752 ExplorationPolicy policy;
753 policy.stop_at_first_solution = false;
754
757
758 auto result = engine.search(SubsetSumState(4), collector);
759
760 ASSERT_TRUE(result.found_solution());
761 EXPECT_TRUE(result.exhausted());
762 EXPECT_EQ(result.stats.solutions_found, 2u);
763 EXPECT_EQ(collector.size(), 2u);
764 EXPECT_GT(result.stats.pruned_by_domain, 0u);
765 EXPECT_TRUE(contains_signature(collector.solutions(), "0110",
766 [](const auto &solution)
767 {
768 return subset_sum_signature(solution.state);
769 }));
770 EXPECT_TRUE(contains_signature(collector.solutions(), "0001",
771 [](const auto &solution)
772 {
773 return subset_sum_signature(solution.state);
774 }));
775}
776
778{
779 SubsetSumDomain domain(Array<int>{4, 1, 1, 2}, 2);
780 ExplorationPolicy policy;
781 policy.stop_at_first_solution = false;
782
783 SearchLimits limits;
784 limits.max_solutions = 1;
785
788
789 auto result = engine.search(SubsetSumState(4), collector);
790
791 EXPECT_TRUE(result.limit_reached());
792 EXPECT_EQ(result.stats.solutions_found, 1u);
793 EXPECT_EQ(collector.size(), 1u);
794}
795
797{
798 TinyIDAStarDomain domain;
800
801 auto result = engine.search(TinyIDAStarDomain::State{});
802
803 ASSERT_TRUE(result.found_solution());
804 EXPECT_EQ(result.total_cost, 6);
805 ASSERT_TRUE(result.best_solution.has_value());
806 EXPECT_EQ(result.best_solution.get().path.size(), 2u);
807 EXPECT_EQ(ida_star_path_signature(result.best_solution.get().path), "014");
808 ASSERT_FALSE(result.iterations.is_empty());
809 EXPECT_EQ(result.iterations[0].threshold, 6);
810}
811
813{
814 TinyIDAStarDomain domain;
815 SearchLimits limits;
816 limits.max_depth = 1;
817
819 auto result = engine.search(TinyIDAStarDomain::State{});
820
821 EXPECT_FALSE(result.found_solution());
822 EXPECT_TRUE(result.exhausted());
823 EXPECT_GT(result.stats.pruned_by_depth, 0u);
824}
825
827{
828 TinyIDAStarDomain domain;
829 ExplorationPolicy policy;
830 policy.stop_at_first_solution = false;
831
832 size_t callbacks = 0;
833 auto on_solution = [&](const auto &solution) {
834 (void) solution;
835 ++callbacks;
836 return callbacks == 1 ? false : true;
837 };
838
840 auto result = engine.search(TinyIDAStarDomain::State{}, on_solution);
841
842 EXPECT_TRUE(result.stopped_on_solution());
843 EXPECT_EQ(callbacks, 1u);
844 EXPECT_EQ(result.stats.solutions_found, 1u);
845}
846
848{
849 auto undo_called = std::make_shared<bool>(false);
850 ThrowingApplyIDADomain domain;
852
853 EXPECT_THROW((void) engine.search(ThrowingApplyIDAState{undo_called, 0}), std::runtime_error);
854 EXPECT_FALSE(*undo_called);
855}
856
858{
859 auto undo_called = std::make_shared<bool>(false);
860 ThrowingPostApplyIDADomain domain;
862
863 EXPECT_THROW((void) engine.search(ThrowingPostApplyIDAState{undo_called, 0}), std::runtime_error);
864 EXPECT_TRUE(*undo_called);
865}
866
867// ---------------------------------------------------------------------------
868// Edge-case tests (Recommendation 5)
869// ---------------------------------------------------------------------------
870
871// max_depth=0: root is visited but never expanded — no solutions possible
872// in a tree where goals live at depth 2.
874{
875 ArtificialDecisionTreeDomain domain;
876 ExplorationPolicy policy;
877 policy.stop_at_first_solution = false;
878
879 SearchLimits limits;
880 limits.max_depth = 0;
881
883 auto result = engine.search(ArtificialTreeState{});
884
885 EXPECT_FALSE(result.found_solution());
886 EXPECT_TRUE(result.exhausted());
887 EXPECT_EQ(result.stats.visited_states, 1u);
888 EXPECT_EQ(result.stats.expanded_states, 0u);
889 EXPECT_EQ(result.stats.pruned_by_depth, 1u);
890 EXPECT_EQ(result.stats.generated_successors, 0u);
891}
892
893// max_expansions=1: only the root is expanded — its two children are visited
894// but never expanded themselves, so no solutions at depth 2.
896{
897 ArtificialDecisionTreeDomain domain;
898 ExplorationPolicy policy;
899 policy.stop_at_first_solution = false;
900
901 SearchLimits limits;
902 limits.max_expansions = 1;
903
905 auto result = engine.search(ArtificialTreeState{});
906
907 EXPECT_TRUE(result.limit_reached());
908 EXPECT_EQ(result.stats.expanded_states, 1u);
909 EXPECT_GE(result.stats.generated_successors, 1u);
910 EXPECT_LE(result.stats.generated_successors, 2u);
911 EXPECT_EQ(result.stats.solutions_found, 0u);
912}
913
914// Root is goal: a domain where the initial state already satisfies is_goal().
915// The engine must find a solution with an empty path at depth 0.
916namespace
917{
918
919struct RootGoalDomain
920{
921 struct Move
922 {
923 int id = 0;
924 };
925
926 using State = ArtificialTreeState;
927 using State_Key = size_t;
928
929 [[nodiscard]] State_Key state_key(const State &state) const noexcept
930 {
931 return state.code;
932 }
933
934 bool is_goal(const State &) const
935 {
936 return true;
937 }
938
939 void apply(State &state, const Move &) const
940 {
941 ++state.depth;
942 }
943
944 void undo(State &state, const Move &) const
945 {
946 --state.depth;
947 }
948
949 template <typename Visitor>
950 bool for_each_successor(const State &, Visitor visit) const
951 {
952 return visit(Move{1});
953 }
954};
955
956} // end namespace
957
959{
960 RootGoalDomain domain;
962
963 auto result = engine.search(ArtificialTreeState{});
964
965 ASSERT_TRUE(result.found_solution());
966 EXPECT_TRUE(result.stopped_on_solution());
967 EXPECT_EQ(result.stats.solutions_found, 1u);
968 EXPECT_EQ(result.stats.visited_states, 1u);
969 EXPECT_EQ(result.stats.expanded_states, 0u);
970 EXPECT_EQ(result.best_solution.get().depth, 0u);
971 EXPECT_TRUE(result.best_solution.get().path.is_empty());
972}
973
974// Root is goal with max_depth=0: even at depth 0, the root is checked for
975// goal before the depth limit prunes expansion — it should still find a solution.
977{
978 RootGoalDomain domain;
979 SearchLimits limits;
980 limits.max_depth = 0;
981
983
984 auto result = engine.search(ArtificialTreeState{});
985
986 ASSERT_TRUE(result.found_solution());
987 EXPECT_EQ(result.stats.solutions_found, 1u);
988 EXPECT_EQ(result.best_solution.get().depth, 0u);
989}
990
991// Cycles in the state space: a directed graph with cycles that would cause
992// infinite recursion without a visited set. The graph is:
993//
994// 0 → 1 → 2 → 3 (goal)
995// ↘ 0 (cycle back)
996//
997// Without visited-set, the cycle 0→1→0→1→... causes infinite recursion.
998// With visited-set (search_visited), the engine detects the repeated state
999// and finds the goal via the non-cyclic path.
1000namespace
1001{
1002
1003struct CyclicGraphState
1004{
1005 size_t node = 0;
1006};
1007
1008struct CyclicGraphDomain
1009{
1010 struct Move
1011 {
1012 size_t from = 0;
1013 size_t to = 0;
1014 };
1015
1016 using State = CyclicGraphState;
1017 using State_Key = size_t;
1018
1019 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1020 {
1021 return state.node;
1022 }
1023
1024 bool is_goal(const State &state) const
1025 {
1026 return state.node == 3;
1027 }
1028
1029 void apply(State &state, const Move &move) const
1030 {
1031 state.node = move.to;
1032 }
1033
1034 void undo(State &state, const Move &move) const
1035 {
1036 state.node = move.from;
1037 }
1038
1039 template <typename Visitor>
1040 bool for_each_successor(const State &state, Visitor visit) const
1041 {
1042 switch (state.node)
1043 {
1044 case 0:
1045 return visit(Move{0, 1});
1046 case 1:
1047 if (not visit(Move{1, 0})) // cycle back to 0 (tried first)
1048 return false;
1049 return visit(Move{1, 2});
1050 case 2:
1051 return visit(Move{2, 3});
1052 default:
1053 return true;
1054 }
1055 }
1056};
1057
1058} // end namespace
1059
1061{
1062 CyclicGraphDomain domain;
1064
1066 auto result = engine.search(CyclicGraphState{}, visited);
1067
1068 ASSERT_TRUE(result.found_solution());
1069 EXPECT_EQ(result.best_solution.get().state.node, 3u);
1070 EXPECT_GT(result.stats.pruned_by_visited, 0u);
1071}
1072
1073// Cycle with multiple paths of different depth: state S is reachable via a
1074// short path (depth 1) and a long path (depth 3). With the depth-aware
1075// visited map, the shorter path records depth 1 for S. When the longer path
1076// reaches S at depth 3, it should be pruned. Goal G is reachable only
1077// through S.
1078//
1079// 0 → S → G (short: depth 2)
1080// 0 → A → B → S → G (long: depth 4, pruned at S)
1081//
1082namespace
1083{
1084
1085struct MultiPathState
1086{
1087 int node = 0;
1088};
1089
1090struct MultiPathDomain
1091{
1092 struct Move
1093 {
1094 int from = 0;
1095 int to = 0;
1096 };
1097
1098 using State = MultiPathState;
1099 using State_Key = int;
1100
1101 static constexpr int S = 1;
1102 static constexpr int G = 2;
1103 static constexpr int A = 3;
1104 static constexpr int B = 4;
1105
1106 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1107 {
1108 return state.node;
1109 }
1110
1111 bool is_goal(const State &state) const
1112 {
1113 return state.node == G;
1114 }
1115
1116 void apply(State &state, const Move &move) const
1117 {
1118 state.node = move.to;
1119 }
1120
1121 void undo(State &state, const Move &move) const
1122 {
1123 state.node = move.from;
1124 }
1125
1126 template <typename Visitor>
1127 bool for_each_successor(const State &state, Visitor visit) const
1128 {
1129 switch (state.node)
1130 {
1131 case 0:
1132 if (not visit(Move{0, S})) // short path: 0 → S
1133 return false;
1134 return visit(Move{0, A}); // long path: 0 → A → B → S
1135 case S:
1136 return visit(Move{S, G});
1137 case A:
1138 return visit(Move{A, B});
1139 case B:
1140 return visit(Move{B, S}); // reaches S at depth 3 (pruned)
1141 default:
1142 return true;
1143 }
1144 }
1145};
1146
1147} // end namespace
1148
1150{
1151 MultiPathDomain domain;
1152 ExplorationPolicy policy;
1153 policy.stop_at_first_solution = false;
1154
1156
1158 auto result = engine.search(MultiPathState{}, visited);
1159
1160 ASSERT_TRUE(result.found_solution());
1161 EXPECT_EQ(result.stats.solutions_found, 1u);
1162 EXPECT_EQ(result.stats.pruned_by_visited, 1u);
1163 EXPECT_EQ(result.best_solution.get().state.node, MultiPathDomain::G);
1164}
1165
1166// IDA* with max_depth=0: root is visited but not expanded.
1168{
1169 TinyIDAStarDomain domain;
1170 SearchLimits limits;
1171 limits.max_depth = 0;
1172
1174 auto result = engine.search(TinyIDAStarDomain::State{});
1175
1176 EXPECT_FALSE(result.found_solution());
1177 EXPECT_TRUE(result.exhausted());
1178 EXPECT_GT(result.stats.pruned_by_depth, 0u);
1179 EXPECT_EQ(result.stats.expanded_states, 0u);
1180}
1181
1182// IDA* with root as goal: the initial state has node==goal.
1183namespace
1184{
1185
1186struct IDAStarRootGoalDomain
1187{
1188 struct Move
1189 {
1190 int to = 0;
1191 int cost = 1;
1192 };
1193
1194 using State = TinyIDAStarState;
1195 using State_Key = size_t;
1196 using Distance = int;
1197
1198 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1199 {
1200 return state.node;
1201 }
1202
1203 bool is_goal(const State &state) const
1204 {
1205 return state.node == 0;
1206 }
1207
1208 bool is_terminal(const State &) const
1209 {
1210 return false;
1211 }
1212
1213 void apply(State &state, const Move &move) const
1214 {
1215 state.history.append(state.node);
1216 state.node = static_cast<size_t>(move.to);
1217 }
1218
1219 void undo(State &state, const Move &) const
1220 {
1221 if (state.history.is_empty())
1222 return;
1223
1224 const size_t prev = state.history[state.history.size() - 1];
1225 (void) state.history.remove_last();
1226 state.node = prev;
1227 }
1228
1229 template <typename Visitor>
1230 bool for_each_successor(const State &, Visitor visit) const
1231 {
1232 return visit(Move{1, 5});
1233 }
1234
1235 [[nodiscard]] Distance heuristic(const State &) const noexcept
1236 {
1237 return 0;
1238 }
1239
1240 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1241 {
1242 return move.cost;
1243 }
1244};
1245
1246} // end namespace
1247
1249{
1250 IDAStarRootGoalDomain domain;
1252
1253 auto result = engine.search(TinyIDAStarState{});
1254
1255 ASSERT_TRUE(result.found_solution());
1256 EXPECT_EQ(result.total_cost, 0);
1257 EXPECT_EQ(result.stats.solutions_found, 1u);
1258 EXPECT_TRUE(result.best_solution.get().path.is_empty());
1259}
1260
1261// IDA* with zero-cost edges: all edges cost 0. The heuristic is 0 everywhere.
1262// This means f = g + h = 0 for all states and the initial threshold suffices.
1263namespace
1264{
1265
1266struct ZeroCostIDADomain
1267{
1268 struct Move
1269 {
1270 size_t to = 0;
1271 int cost = 0;
1272 };
1273
1274 using State = TinyIDAStarState;
1275 using State_Key = size_t;
1276 using Distance = int;
1277
1278 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1279 {
1280 return state.node;
1281 }
1282
1283 bool is_goal(const State &state) const
1284 {
1285 return state.node == 2;
1286 }
1287
1288 void apply(State &state, const Move &move) const
1289 {
1290 state.history.append(state.node);
1291 state.node = move.to;
1292 }
1293
1294 void undo(State &state, const Move &) const
1295 {
1296 if (state.history.is_empty())
1297 return;
1298
1299 const size_t prev = state.history[state.history.size() - 1];
1300 (void) state.history.remove_last();
1301 state.node = prev;
1302 }
1303
1304 template <typename Visitor>
1305 bool for_each_successor(const State &state, Visitor visit) const
1306 {
1307 if (state.node == 0)
1308 return visit(Move{1, 0});
1309 if (state.node == 1)
1310 return visit(Move{2, 0});
1311 return true;
1312 }
1313
1314 [[nodiscard]] Distance heuristic(const State &) const noexcept
1315 {
1316 return 0;
1317 }
1318
1319 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1320 {
1321 return move.cost;
1322 }
1323};
1324
1325} // end namespace
1326
1328{
1329 ZeroCostIDADomain domain;
1331
1332 auto result = engine.search(TinyIDAStarState{});
1333
1334 ASSERT_TRUE(result.found_solution());
1335 EXPECT_EQ(result.total_cost, 0);
1336 EXPECT_EQ(result.stats.solutions_found, 1u);
1337}
1338
1339// NQueens n=1: trivial single-queen problem — should find exactly 1 solution.
1341{
1342 NQueensDomain domain{0};
1343 ExplorationPolicy policy;
1344 policy.stop_at_first_solution = false;
1345
1348
1349 auto result = engine.search(NQueensState(0), collector);
1350
1351 ASSERT_TRUE(result.found_solution());
1352 EXPECT_TRUE(result.exhausted());
1353 EXPECT_EQ(result.stats.solutions_found, 1u);
1354 EXPECT_EQ(collector.size(), 1u);
1355 EXPECT_EQ(nqueens_signature(result.best_solution.get().state), "");
1356}
1357
1358// NQueens n=1: trivial single-queen problem — should find exactly 1 solution.
1360{
1361 NQueensDomain domain{1};
1362 ExplorationPolicy policy;
1363 policy.stop_at_first_solution = false;
1364
1367
1368 auto result = engine.search(NQueensState(1), collector);
1369
1370 ASSERT_TRUE(result.found_solution());
1371 EXPECT_TRUE(result.exhausted());
1372 EXPECT_EQ(result.stats.solutions_found, 1u);
1373 EXPECT_EQ(collector.size(), 1u);
1374 EXPECT_EQ(nqueens_signature(result.best_solution.get().state), "0");
1375}
1376
1377// NQueens n=8: realistic problem — should find all 92 solutions.
1379{
1380 NQueensDomain domain{8};
1381 ExplorationPolicy policy;
1382 policy.stop_at_first_solution = false;
1383
1386
1387 auto result = engine.search(NQueensState(8), collector);
1388
1389 ASSERT_TRUE(result.found_solution());
1390 EXPECT_TRUE(result.exhausted());
1391 EXPECT_EQ(result.stats.solutions_found, 92u);
1392 EXPECT_EQ(collector.size(), 92u);
1393}
1394
1395// H2: SearchStorageSet runtime sanity — basic contains/insert contract.
1397{
1399
1400 EXPECT_FALSE(visited.contains(1u));
1401 visited.insert(1u);
1402 EXPECT_TRUE(visited.contains(1u));
1403 EXPECT_FALSE(visited.contains(2u));
1404
1405 // Inserting the same key twice is a no-op (no duplicate).
1406 visited.insert(1u);
1407 EXPECT_TRUE(visited.contains(1u));
1408}
1409
1410// H5a: IDA* on a graph with no path to the goal terminates with Exhausted.
1411//
1412// Graph: 0 → 1 (only node 0 has a successor; goal is unreachable node 9)
1413namespace
1414{
1415
1416struct NoPathIDADomain
1417{
1418 struct Move
1419 {
1420 size_t to = 0;
1421 double cost = 1.0;
1422 };
1423
1424 using State = TinyIDAStarState;
1425 using State_Key = size_t;
1426 using Distance = double;
1427
1428 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1429 {
1430 return state.node;
1431 }
1432
1433 [[nodiscard]] bool is_goal(const State &state) const noexcept
1434 {
1435 return state.node == 9; // unreachable
1436 }
1437
1438 void apply(State &state, const Move &move) const
1439 {
1440 state.history.append(state.node);
1441 state.node = move.to;
1442 }
1443
1444 void undo(State &state, const Move &) const
1445 {
1446 if (state.history.is_empty())
1447 return;
1448 const size_t prev = state.history[state.history.size() - 1];
1449 (void) state.history.remove_last();
1450 state.node = prev;
1451 }
1452
1453 template <typename Visitor>
1454 bool for_each_successor(const State &state, Visitor visit) const
1455 {
1456 // Only node 0 has a successor; node 1 is a dead end.
1457 if (state.node == 0)
1458 return visit(Move{1, 1});
1459 return true;
1460 }
1461
1462 [[nodiscard]] Distance heuristic(const State &) const noexcept
1463 {
1464 return 1.0; // admissible: never overestimates for unreachable goal
1465 }
1466
1467 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1468 {
1469 return move.cost;
1470 }
1471};
1472
1473// H5b: domain with an inadmissible heuristic (overestimates).
1474// The IDA* engine still terminates and finds a solution, but the returned
1475// cost may be higher than the true optimum because the overestimating
1476// heuristic prunes the optimal path.
1477//
1478// Graph: 0 --1--> 1 --1--> 2 (goal) optimal = 2
1479// Heuristic: h(0)=100, h(1)=100, h(2)=0 (overestimates massively)
1480// With inadmissible h, the initial threshold = 100, so the first pass
1481// already visits all states and finds the solution at cost 2.
1482struct InadmissibleHeuristicDomain
1483{
1484 struct Move
1485 {
1486 size_t to = 0;
1487 double cost = 1.0;
1488 };
1489
1490 using State = TinyIDAStarState;
1491 using State_Key = size_t;
1492 using Distance = double;
1493
1494 [[nodiscard]] State_Key state_key(const State &state) const noexcept
1495 {
1496 return state.node;
1497 }
1498
1499 [[nodiscard]] bool is_goal(const State &state) const noexcept
1500 {
1501 return state.node == 2;
1502 }
1503
1504 void apply(State &state, const Move &move) const
1505 {
1506 state.history.append(state.node);
1507 state.node = move.to;
1508 }
1509
1510 void undo(State &state, const Move &) const
1511 {
1512 if (state.history.is_empty())
1513 return;
1514 const size_t prev = state.history[state.history.size() - 1];
1515 (void) state.history.remove_last();
1516 state.node = prev;
1517 }
1518
1519 template <typename Visitor>
1520 bool for_each_successor(const State &state, Visitor visit) const
1521 {
1522 if (state.node == 0)
1523 return visit(Move{1, 1});
1524 if (state.node == 1)
1525 return visit(Move{2, 1});
1526 return true;
1527 }
1528
1529 [[nodiscard]] Distance heuristic(const State &state) const noexcept
1530 {
1531 // Massively inadmissible for non-goal nodes.
1532 return state.node == 2 ? 0.0 : 100.0;
1533 }
1534
1535 [[nodiscard]] Distance cost(const State &, const Move &move) const noexcept
1536 {
1537 return move.cost;
1538 }
1539};
1540
1541} // end namespace
1542
1544{
1545 NoPathIDADomain domain;
1547
1548 auto result = engine.search(TinyIDAStarState{});
1549
1550 EXPECT_FALSE(result.found_solution());
1551 EXPECT_TRUE(result.exhausted());
1552 ASSERT_FALSE(result.iterations.is_empty());
1553 EXPECT_EQ(result.iterations[result.iterations.size() - 1].next_threshold,
1554 ida_star_detail::distance_unreachable<double>());
1555}
1556
1558{
1559 InadmissibleHeuristicDomain domain;
1561
1562 auto result = engine.search(TinyIDAStarState{});
1563
1564 // With an inadmissible heuristic IDA* is no longer guaranteed optimal,
1565 // but it must still find a solution if one exists.
1566 ASSERT_TRUE(result.found_solution());
1567 EXPECT_DOUBLE_EQ(result.total_cost, 2.0);
1568}
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: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
constexpr bool is_empty() const noexcept
Checks if the container is empty.
Definition tpl_array.H:348
T & append(const T &data)
Append a copy of data
Definition tpl_array.H:245
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:428
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
constexpr size_t Search_Unlimited
Sentinel used by SearchLimits to mean "no bound".
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.
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