69 bool validate =
false;
71 size_t repetitions = 32;
79 double median_ms = 0.0;
82 double stddev_ms = 0.0;
92 size_t visited_states = 0;
93 size_t expanded_states = 0;
94 size_t generated_successors = 0;
95 size_t solutions_found = 0;
96 size_t terminal_states = 0;
97 size_t pruned_by_depth = 0;
98 size_t pruned_by_domain = 0;
99 size_t pruned_by_bound = 0;
100 size_t incumbent_updates = 0;
101 size_t alpha_beta_cutoffs = 0;
103 size_t tt_cutoffs = 0;
104 size_t tt_stores = 0;
105 size_t ordered_batches = 0;
106 size_t priority_estimates = 0;
107 size_t killer_hits = 0;
108 size_t history_hits = 0;
110 double result_value = 0.0;
111 bool has_best_move =
false;
112 size_t best_move = 0;
120 for (
int i = 1; i <
argc; ++i)
122 const std::string
arg =
argv[i];
123 if (
arg ==
"--validate")
125 else if (
arg ==
"--json")
127 else if (
arg ==
"--repetitions" and i + 1 <
argc)
128 options.repetitions =
static_cast<size_t>(std::stoull(
argv[++i]));
130 options.warmup =
static_cast<size_t>(std::stoull(
argv[++i]));
132 options.samples =
static_cast<size_t>(std::stoull(
argv[++i]));
133 else if (
arg ==
"--help")
136 <<
"Usage: state_search_benchmark [--validate] [--json]"
137 <<
" [--repetitions N] [--warmup N] [--samples N]\n";
142 std::cerr <<
"Unknown option: " <<
arg <<
'\n';
149 std::cerr <<
"Repetitions and samples must be positive\n";
170 stats.mean_ms =
sum/
static_cast<double>(
sample_ms.size());
174 if (
sorted.size() % 2 == 0)
184 const double delta = value - stats.mean_ms;
188 stats.stddev_ms = std::sqrt(sq/
static_cast<double>(
sample_ms.size() - 1));
194template <
typename Runner,
typename Capture>
197 const std::string &scenario,
198 const std::string &variant,
203 for (
size_t i = 0; i <
options.warmup; ++i)
208 row.scenario = scenario;
209 row.variant = variant;
214 for (
size_t sample = 0; sample <
options.samples; ++sample)
216 double total_ms = 0.0;
219 auto result = runner();
221 total_ms += result.stats.elapsed_ms;
222 benchmark_sink +=
static_cast<long double>(result.stats.visited_states +
223 result.stats.generated_successors +
224 result.stats.expanded_states);
236 const std::string &scenario,
237 const std::string &variant)
239 for (
size_t i = 0; i < rows.
size(); ++i)
240 if (rows[i].scenario == scenario
and rows[i].variant == variant)
255 explicit NQueensState(
const size_t size = 0)
259 used_columns(
size, 0),
260 used_diag_down(
size == 0 ? 0 : 2*
size - 1, 0),
261 used_diag_up(
size == 0 ? 0 : 2*
size - 1, 0)
274 using State = NQueensState;
278 bool is_goal(
const State &state)
const
280 return state.row == n;
283 void apply(State &state,
const Move &move)
const
285 const size_t row = state.row;
286 const size_t down = row + state.n - 1 - move.col;
287 const size_t up = row + move.col;
289 state.queens[row] =
static_cast<int>(move.col);
290 state.used_columns[move.col] = 1;
291 state.used_diag_down[
down] = 1;
292 state.used_diag_up[
up] = 1;
296 void undo(State &state,
const Move &move)
const
299 const size_t row = state.row;
300 const size_t down = row + state.n - 1 - move.col;
301 const size_t up = row + move.col;
303 state.queens[row] = -1;
304 state.used_columns[move.col] = 0;
305 state.used_diag_down[
down] = 0;
306 state.used_diag_up[
up] = 0;
309 template <
typename Visitor>
310 bool for_each_successor(
const State &state,
Visitor visit)
const
315 for (
size_t col = 0; col < n; ++col)
317 const size_t down = state.row + state.n - 1 - col;
318 const size_t up = state.row + col;
319 if (state.used_columns[col]
or state.used_diag_down[
down]
or state.used_diag_up[
up])
376 int remaining =
capacity_ - state.weight;
378 for (
size_t i = state.index; i <
items_.
size()
and remaining > 0; ++i)
379 if (
items_[i].weight <= remaining)
382 remaining -=
items_[i].weight;
394 void apply(
State &state,
const Move &move)
const
398 state.weight +=
items_[state.index].weight;
399 state.value +=
items_[state.index].value;
400 state.chosen[state.index] = 1;
403 state.chosen[state.index] = 0;
408 void undo(
State &state,
const Move &move)
const
413 state.weight -=
items_[state.index].weight;
414 state.value -=
items_[state.index].value;
417 state.chosen[state.index] = 0;
420 template <
typename Visitor>
433 return visit(Move{
false});
440 return visit(Move{
true});
448 bool take_first_ =
true;
455 size_t moves_played = 0;
458 : board(size_t(9), 0)
474 using State = TicTacToeState;
476 using State_Key = std::uint64_t;
477 using Move_Key = size_t;
481 return winner(state) != 0
or state.moves_played == 9;
484 Score
evaluate(
const State &state)
const
488 return win == state.player ? 100 : -100;
490 if (state.moves_played == 9)
493 return heuristic(state, state.player) - heuristic(state, -state.player);
496 [[
nodiscard]] State_Key state_key(
const State &state)
const noexcept
498 State_Key key =
static_cast<State_Key
>(state.player > 0 ? 1 : 2);
499 for (
size_t i = 0; i < state.board.size(); ++i)
502 if (state.board[i] == 1)
504 else if (state.board[i] == -1)
516 void apply(State &state,
const Move &move)
const
518 state.board[move.cell] = state.player;
519 state.player = -state.player;
520 ++state.moves_played;
523 void undo(State &state,
const Move &move)
const
525 --state.moves_played;
526 state.player = -state.player;
527 state.board[move.cell] = 0;
530 template <
typename Visitor>
531 bool for_each_successor(
const State &state,
Visitor visit)
const
533 static constexpr size_t order[] = {1, 3, 5, 7, 0, 2, 6, 8, 4};
535 for (
const size_t cell : order)
545 static constexpr size_t lines[8][3] = {
546 {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
547 {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
551 for (
const auto &line :
lines)
553 const int a = state.board[line[0]];
554 if (a != 0
and a == state.board[line[1]]
and a == state.board[line[2]])
561 [[
nodiscard]]
static Score heuristic(
const State &state,
const int player)
noexcept
563 static constexpr size_t lines[8][3] = {
564 {0, 1, 2}, {3, 4, 5}, {6, 7, 8},
565 {0, 3, 6}, {1, 4, 7}, {2, 5, 8},
570 for (
const auto &line :
lines)
574 bool blocked =
false;
576 for (
const size_t cell : line)
577 if (state.board[cell] == player)
579 else if (state.board[cell] == 0)
592 else if (
mine == 1
and empty == 2)
604 {2, 30.0}, {3, 42.0}, {4, 52.0}, {5, 60.0},
605 {6, 66.0}, {7, 70.0}, {8, 72.0}, {9, 72.0},
606 {10, 70.0}, {11, 66.0}, {12, 60.0}, {13, 52.0}
613 constexpr size_t n = 8;
627 return Search::backtracking_search(NQueensDomain{n}, NQueensState(n), policy, limits);
629 [](BenchmarkRow &row,
const auto &result)
631 row.visited_states = result.stats.visited_states;
632 row.expanded_states = result.stats.expanded_states;
633 row.generated_successors = result.stats.generated_successors;
634 row.solutions_found = result.stats.solutions_found;
635 row.terminal_states = result.stats.terminal_states;
636 row.pruned_by_depth = result.stats.pruned_by_depth;
637 row.pruned_by_domain = result.stats.pruned_by_domain;
638 row.result_value =
static_cast<double>(result.stats.solutions_found);
648 constexpr int capacity = 35;
663 return Search::branch_and_bound_search(
KnapsackDomain(items, capacity,
false),
668 [
optimum](BenchmarkRow &row,
const auto &result)
670 row.visited_states = result.stats.visited_states;
671 row.expanded_states = result.stats.expanded_states;
672 row.generated_successors = result.stats.generated_successors;
673 row.solutions_found = result.stats.solutions_found;
674 row.terminal_states = result.stats.terminal_states;
675 row.pruned_by_depth = result.stats.pruned_by_depth;
676 row.pruned_by_domain = result.stats.pruned_by_domain;
677 row.pruned_by_bound = result.stats.pruned_by_bound;
678 row.incumbent_updates = result.stats.incumbent_updates;
679 row.result_value = result.incumbent.best_value();
686 "depth_first_bound_ordered",
696 return Search::branch_and_bound_search(
KnapsackDomain(items, capacity,
false),
701 [](BenchmarkRow &row,
const auto &result)
703 row.visited_states = result.stats.visited_states;
704 row.expanded_states = result.stats.expanded_states;
705 row.generated_successors = result.stats.generated_successors;
706 row.solutions_found = result.stats.solutions_found;
707 row.terminal_states = result.stats.terminal_states;
708 row.pruned_by_depth = result.stats.pruned_by_depth;
709 row.pruned_by_domain = result.stats.pruned_by_domain;
710 row.pruned_by_bound = result.stats.pruned_by_bound;
711 row.incumbent_updates = result.stats.incumbent_updates;
712 row.ordered_batches = result.stats.move_ordering.ordered_batches;
713 row.priority_estimates = result.stats.move_ordering.priority_estimates;
714 row.result_value = result.incumbent.best_value();
725 policy.
strategy = Search::ExplorationPolicy::Strategy::Best_First;
730 return Search::branch_and_bound_search(
KnapsackDomain(items, capacity,
false),
735 [](BenchmarkRow &row,
const auto &result)
737 row.visited_states = result.stats.visited_states;
738 row.expanded_states = result.stats.expanded_states;
739 row.generated_successors = result.stats.generated_successors;
740 row.solutions_found = result.stats.solutions_found;
741 row.terminal_states = result.stats.terminal_states;
742 row.pruned_by_depth = result.stats.pruned_by_depth;
743 row.pruned_by_domain = result.stats.pruned_by_domain;
744 row.pruned_by_bound = result.stats.pruned_by_bound;
745 row.incumbent_updates = result.stats.incumbent_updates;
746 row.result_value = result.incumbent.best_value();
757 TicTacToeDomain::Move,
758 TicTacToeDomain::Score>;
769 return Search::alpha_beta_search(TicTacToeDomain{},
root);
771 [](BenchmarkRow &row,
const auto &result)
773 row.visited_states = result.stats.visited_states;
774 row.expanded_states = result.stats.expanded_states;
775 row.generated_successors = result.stats.generated_successors;
776 row.terminal_states = result.stats.terminal_states;
777 row.pruned_by_depth = result.stats.pruned_by_depth;
778 row.pruned_by_domain = result.stats.pruned_by_domain;
779 row.alpha_beta_cutoffs = result.stats.alpha_beta_cutoffs;
780 row.result_value =
static_cast<double>(result.value);
781 row.has_best_move = result.has_principal_variation();
782 row.best_move = result.first_move().cell;
788 "alpha_beta_ordered",
796 return Search::alpha_beta_search(TicTacToeDomain{},
root, policy);
798 [](BenchmarkRow &row,
const auto &result)
800 row.visited_states = result.stats.visited_states;
801 row.expanded_states = result.stats.expanded_states;
802 row.generated_successors = result.stats.generated_successors;
803 row.terminal_states = result.stats.terminal_states;
804 row.pruned_by_depth = result.stats.pruned_by_depth;
805 row.pruned_by_domain = result.stats.pruned_by_domain;
806 row.alpha_beta_cutoffs = result.stats.alpha_beta_cutoffs;
807 row.ordered_batches = result.stats.move_ordering.ordered_batches;
808 row.priority_estimates = result.stats.move_ordering.priority_estimates;
809 row.killer_hits = result.stats.move_ordering.killer_hits;
810 row.history_hits = result.stats.move_ordering.history_hits;
811 row.result_value =
static_cast<double>(result.value);
812 row.has_best_move = result.has_principal_variation();
813 row.best_move = result.first_move().cell;
824 return Search::alpha_beta_search(TicTacToeDomain{},
root, table);
826 [](BenchmarkRow &row,
const auto &result)
828 row.visited_states = result.stats.visited_states;
829 row.expanded_states = result.stats.expanded_states;
830 row.generated_successors = result.stats.generated_successors;
831 row.terminal_states = result.stats.terminal_states;
832 row.pruned_by_depth = result.stats.pruned_by_depth;
833 row.pruned_by_domain = result.stats.pruned_by_domain;
834 row.alpha_beta_cutoffs = result.stats.alpha_beta_cutoffs;
835 row.tt_hits = result.stats.transpositions.hits;
836 row.tt_cutoffs = result.stats.transpositions.cutoffs;
837 row.tt_stores = result.stats.transpositions.stores;
838 row.result_value =
static_cast<double>(result.value);
839 row.has_best_move = result.has_principal_variation();
840 row.best_move = result.first_move().cell;
846 "alpha_beta_ordered_tt",
855 return Search::alpha_beta_search(TicTacToeDomain{},
root, table, policy);
857 [](BenchmarkRow &row,
const auto &result)
859 row.visited_states = result.stats.visited_states;
860 row.expanded_states = result.stats.expanded_states;
861 row.generated_successors = result.stats.generated_successors;
862 row.terminal_states = result.stats.terminal_states;
863 row.pruned_by_depth = result.stats.pruned_by_depth;
864 row.pruned_by_domain = result.stats.pruned_by_domain;
865 row.alpha_beta_cutoffs = result.stats.alpha_beta_cutoffs;
866 row.tt_hits = result.stats.transpositions.hits;
867 row.tt_cutoffs = result.stats.transpositions.cutoffs;
868 row.tt_stores = result.stats.transpositions.stores;
869 row.ordered_batches = result.stats.move_ordering.ordered_batches;
870 row.priority_estimates = result.stats.move_ordering.priority_estimates;
871 row.killer_hits = result.stats.move_ordering.killer_hits;
872 row.history_hits = result.stats.move_ordering.history_hits;
873 row.result_value =
static_cast<double>(result.value);
874 row.has_best_move = result.has_principal_variation();
875 row.best_move = result.first_move().cell;
887 const auto *
plain =
find_row(rows,
"tictactoe-full",
"alpha_beta_plain");
888 const auto *
ordered =
find_row(rows,
"tictactoe-full",
"alpha_beta_ordered");
889 const auto *
tt =
find_row(rows,
"tictactoe-full",
"alpha_beta_tt");
895 throw std::runtime_error(
"Benchmark validation failed: missing benchmark row");
897 if (
nqueens->solutions_found != 92u)
898 throw std::runtime_error(
"N-Queens validation failed: expected 92 solutions");
902 throw std::runtime_error(
"Knapsack validation failed: inconsistent optimum");
904 throw std::runtime_error(
"Knapsack validation failed: bound ordering did not help");
906 throw std::runtime_error(
"Knapsack validation failed: best-first did not prune");
909 plain->result_value !=
tt->result_value
or
911 throw std::runtime_error(
"Alpha-Beta validation failed: inconsistent root value");
913 throw std::runtime_error(
"Alpha-Beta validation failed: move ordering did not reduce nodes");
914 if (
tt->visited_states >=
plain->visited_states)
915 throw std::runtime_error(
"Alpha-Beta validation failed: TT did not reduce nodes");
920 std::cout << row.family <<
" / " << row.scenario <<
" / " << row.variant <<
'\n';
921 std::cout <<
" mean_ms: " << row.timing.mean_ms
922 <<
" median_ms: " << row.timing.median_ms
923 <<
" min_ms: " << row.timing.min_ms
924 <<
" max_ms: " << row.timing.max_ms <<
'\n';
925 std::cout <<
" visited: " << row.visited_states
926 <<
" expanded: " << row.expanded_states
927 <<
" generated: " << row.generated_successors <<
'\n';
929 if (row.solutions_found > 0)
930 std::cout <<
" solutions: " << row.solutions_found <<
'\n';
932 if (row.pruned_by_bound > 0)
933 std::cout <<
" pruned_by_bound: " << row.pruned_by_bound
934 <<
" incumbent_updates: " << row.incumbent_updates <<
'\n';
936 if (row.alpha_beta_cutoffs > 0
or row.tt_hits > 0)
937 std::cout <<
" alpha_beta_cutoffs: " << row.alpha_beta_cutoffs
938 <<
" tt_hits: " << row.tt_hits
939 <<
" tt_cutoffs: " << row.tt_cutoffs <<
'\n';
941 if (row.ordered_batches > 0
or row.priority_estimates > 0)
942 std::cout <<
" ordered_batches: " << row.ordered_batches
943 <<
" priority_estimates: " << row.priority_estimates
944 <<
" killer_hits: " << row.killer_hits
945 <<
" history_hits: " << row.history_hits <<
'\n';
947 std::cout <<
" result_value: " << row.result_value;
948 if (row.has_best_move)
949 std::cout <<
" best_move: " << row.best_move;
956 std::cout <<
" \"repetitions\": " <<
options.repetitions <<
",\n";
957 std::cout <<
" \"warmup\": " <<
options.warmup <<
",\n";
958 std::cout <<
" \"samples\": " <<
options.samples <<
",\n";
959 std::cout <<
" \"rows\": [\n";
961 for (
size_t i = 0; i < rows.
size(); ++i)
963 const auto &row = rows[i];
966 <<
" \"family\": \"" << row.family <<
"\",\n"
967 <<
" \"scenario\": \"" << row.scenario <<
"\",\n"
968 <<
" \"variant\": \"" << row.variant <<
"\",\n"
969 <<
" \"mean_ms\": " << row.timing.mean_ms <<
",\n"
970 <<
" \"median_ms\": " << row.timing.median_ms <<
",\n"
971 <<
" \"min_ms\": " << row.timing.min_ms <<
",\n"
972 <<
" \"max_ms\": " << row.timing.max_ms <<
",\n"
973 <<
" \"stddev_ms\": " << row.timing.stddev_ms <<
",\n"
974 <<
" \"visited_states\": " << row.visited_states <<
",\n"
975 <<
" \"expanded_states\": " << row.expanded_states <<
",\n"
976 <<
" \"generated_successors\": " << row.generated_successors <<
",\n"
977 <<
" \"solutions_found\": " << row.solutions_found <<
",\n"
978 <<
" \"pruned_by_bound\": " << row.pruned_by_bound <<
",\n"
979 <<
" \"alpha_beta_cutoffs\": " << row.alpha_beta_cutoffs <<
",\n"
980 <<
" \"tt_hits\": " << row.tt_hits <<
",\n"
981 <<
" \"tt_cutoffs\": " << row.tt_cutoffs <<
",\n"
982 <<
" \"ordered_batches\": " << row.ordered_batches <<
",\n"
983 <<
" \"priority_estimates\": " << row.priority_estimates <<
",\n"
984 <<
" \"result_value\": " << row.result_value <<
",\n"
985 <<
" \"has_best_move\": " << (row.has_best_move ?
"true" :
"false") <<
",\n"
986 <<
" \"best_move\": " << row.best_move <<
'\n'
988 if (i + 1 != rows.
size())
993 std::cout <<
" ]\n}\n";
1014 std::cout << std::fixed << std::setprecision(3);
1020 std::cout <<
"State-space search benchmark suite\n";
1021 std::cout <<
"repetitions=" <<
options.repetitions
1022 <<
" warmup=" <<
options.warmup
1023 <<
" samples=" <<
options.samples <<
"\n\n";
1025 for (
const auto &row : rows)
1032 std::cout <<
"validation: ok\n";
Classical knapsack problem variants (0/1, unbounded, bounded).
Umbrella header for the implicit state-space search framework.
static ExplorationPolicy default_policy() noexcept
Return the default exploration policy for Alpha-Beta.
Simple dynamic array with automatic resizing and functional operations.
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
T & append(const T &data)
Append a copy of data
void reserve(size_t cap)
Reserves cap cells into the array.
static ExplorationPolicy default_policy() noexcept
Return the default branch-and-bound exploration policy.
Transposition-table container built on top of Aleph hash maps.
void undo(State &s, const Move &m) const noexcept
Objective bound(const State &s) const noexcept
Greedy fractional upper bound on the remaining items.
bool for_each_successor(const State &s, Visitor visit) const
void apply(State &s, const Move &m) const noexcept
bool is_complete(const State &s) const noexcept
True when all items have been decided.
Objective objective_value(const State &s) const noexcept
Objective value for a complete assignment.
__gmp_expr< T, __gmp_binary_expr< __gmp_expr< T, U >, unsigned long int, __gmp_root_function > > root(const __gmp_expr< T, U > &expr, unsigned long int l)
User-facing facade exported by the umbrella search header.
and
Check uniqueness with explicit hash + equality functors.
bool operator==(const DynList< T > &l1, const DynList< T > &l2)
Equality operator for DynList.
size_t size(Node *root) noexcept
Divide_Conquer_DP_Result< Cost > divide_and_conquer_partition_dp(const size_t groups, const size_t n, Transition_Cost_Fn transition_cost, const Cost inf=dp_optimization_detail::default_inf< Cost >())
Optimize partition DP using divide-and-conquer optimization.
DynList< T > rep(const size_t n, const T &item)
Create a sequence of repeated items.
@ Estimated_Bound
Rank successors by an optimistic child bound.
@ Estimated_Score
Rank successors by a cheap heuristic score estimate.
void introsort(T *a, const long l, const long r, const Compare &cmp=Compare())
Sort an array using introsort (introspective sort).
T sum(const Container &container, const T &init=T{})
Compute sum of all elements.
static struct argp_option options[]
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.
bool use_history_heuristic
Enable experimental history heuristic where supported.
bool use_killer_moves
Enable experimental killer heuristic where supported.
An item for knapsack problems.
Hard bounds applied by the search engine.
size_t max_depth
Maximum expansion depth.
Dynamic array container with automatic resizing.
Comprehensive sorting algorithms and search utilities for Aleph-w.