Skip to content

Commit ea08db9

Browse files
[PE] Improve cost-based pruning in HS.
We can improve cost-based pruning in heuristic search when the heuristic is admissible: we can extend the check ```cpp state.g() > least_path_cost ``` to ```cpp state.g() + h > least_path_cost ``` This is still complete and optimal, since *h* was computed by an admissible heuristic and hence never over-estimates the cost of the best path from the current state to the goal. Therefore, we know that the remaining cost to reach the goal is *at least h*.
1 parent 25770b1 commit ea08db9

File tree

2 files changed

+27
-7
lines changed

2 files changed

+27
-7
lines changed

include/mutable/util/HeuristicSearch.hpp

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ concept heuristic_search_heuristic =
5858
{ H.operator()(S, ctx...) } -> std::convertible_to<double>;
5959
};
6060

61+
template<typename Heurisitc>
62+
concept is_admissible = (Heurisitc::is_admissible);
63+
6164

6265
/*===== Search Algorithm =============================================================================================*/
6366

@@ -85,6 +88,7 @@ concept heuristic_search =
8588
template<
8689
heuristic_search_state State,
8790
typename Expand,
91+
typename Heurisitc,
8892
bool HasRegularQueue,
8993
bool HasBeamQueue,
9094
typename Config,
@@ -252,9 +256,12 @@ struct StateManager
252256
static_assert(ToBeamQueue or HasRegularQueue, "not ToBeamQueue implies HasRegularQueue");
253257

254258
if constexpr (enable_cost_based_pruning) {
255-
/*----- Fast path: if the current state is already more costly than the cheapest complete path found so far,
256-
* we can safely ignore that state. -----*/
257-
if (state.g() >= least_path_cost) [[unlikely]] { // initially unlikely, as we first have to find *a* path
259+
/* Early pruning: if the current state is already more costly than the cheapest complete path found so far,
260+
* we can safely ignore that state. Additionally, if the heuristic is admissible, we know that the
261+
* remaining cost from the current state to the goal is *at least* `h`. Therefore, any complete path from
262+
* start to goal that contains this state has cost at least `g + h`. */
263+
const auto min_path_cost = M_CONSTEXPR_COND(is_admissible<Heurisitc>, state.g() + h, state.g());
264+
if (min_path_cost >= least_path_cost) [[unlikely]] {
258265
inc_pruned_by_cost();
259266
return;
260267
} else if (expand_type::is_goal(state, context...)) [[unlikely]] {
@@ -330,9 +337,12 @@ struct StateManager
330337
push<false>(std::move(state), h, context...);
331338
} else {
332339
if constexpr (enable_cost_based_pruning) {
333-
/*----- Fast path: if the current state is already more costly than the cheapest complete path found
334-
* so far, we can safely ignore that state. -----*/
335-
if (state.g() >= least_path_cost) [[unlikely]] {
340+
/* Early pruning: if the current state is already more costly than the cheapest complete path found
341+
* so far, we can safely ignore that state. Additionally, if the heuristic is admissible, we know
342+
* that the remaining cost from the current state to the goal is *at least* `h`. Therefore, any
343+
* complete path from start to goal that contains this state has cost at least `g + h`. */
344+
const auto min_path_cost = M_CONSTEXPR_COND(is_admissible<Heurisitc>, state.g() + h, state.g());
345+
if (min_path_cost >= least_path_cost) [[unlikely]] {
336346
inc_pruned_by_cost();
337347
return;
338348
} else if (expand_type::is_goal(state, context...)) [[unlikely]] {
@@ -450,12 +460,13 @@ struct StateManager
450460
template<
451461
heuristic_search_state State,
452462
typename Expand,
463+
typename Heurisitc,
453464
bool HasRegularQueue,
454465
bool HasBeamQueue,
455466
typename Config,
456467
typename... Context
457468
>
458-
bool StateManager<State, Expand, HasRegularQueue, HasBeamQueue, Config, Context...>::comparator::
469+
bool StateManager<State, Expand, Heurisitc, HasRegularQueue, HasBeamQueue, Config, Context...>::comparator::
459470
operator()(StateManager::pointer_type p_left, StateManager::pointer_type p_right) const
460471
{
461472
auto left = static_cast<typename map_type::value_type*>(p_left);
@@ -547,6 +558,7 @@ struct genericAStar
547558

548559
StateManager</* State= */ State,
549560
/* Expand= */ Expand,
561+
/* Heurisitc= */ Heuristic,
550562
/* HasRegularQueue= */ not (use_beam_search and is_monotone),
551563
/* HasBeamQueue= */ use_beam_search,
552564
/* Config= */ Config,

src/IR/HeuristicSearchPlanEnumerator.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,8 @@ struct zero
17261726
{
17271727
using state_type = State;
17281728

1729+
static constexpr bool is_admissible = true;
1730+
17291731
zero(const PlanTable&, const QueryGraph&, const AdjacencyMatrix&, const CostFunction&, const CardinalityEstimator&)
17301732
{ }
17311733

@@ -1750,6 +1752,10 @@ struct sum<PlanTable, State, BottomUp>
17501752
{
17511753
using state_type = State;
17521754

1755+
/** For bottom up, the sum heuristic is actually not admissible. The sizes of the subproblems in the current state
1756+
* can be significantly larger than the sizes of the join results, hence over-estimating the remaining cost. */
1757+
static constexpr bool is_admissible = false;
1758+
17531759
sum(const PlanTable&, const QueryGraph&, const AdjacencyMatrix&, const CostFunction&, const CardinalityEstimator&)
17541760
{ }
17551761

@@ -1771,6 +1777,8 @@ struct sum<PlanTable, State, TopDown>
17711777
{
17721778
using state_type = State;
17731779

1780+
static constexpr bool is_admissible = true;
1781+
17741782
sum(const PlanTable&, const QueryGraph&, const AdjacencyMatrix&, const CostFunction&, const CardinalityEstimator&)
17751783
{ }
17761784

0 commit comments

Comments
 (0)