solver: add optional component-area pruning

Full-height skyline columns partition the remaining board. Add sound gcd and bounded subset-sum checks for the resulting component areas, with boundary-event and periodic benchmark schedules.

Keep the rules disabled by default because their small tree reductions do not recover their measured cost. Record the rejected default and scheduling evidence so it can be revisited only with new data.

Tests: Debug CTest (14 passed)

Tests: ASan+UBSan CTest (14 passed)

Refs: #13
This commit was merged in pull request #28.
This commit is contained in:
Codex instance
2026-07-31 08:40:39 +01:00
parent 220cec06a9
commit 39ff5cb340
6 changed files with 491 additions and 53 deletions
+226 -37
View File
@@ -11,6 +11,7 @@
#include <string_view>
#include <vector>
#include <iostream>
#include <numeric>
namespace {
using size_t = std::uint64_t;
@@ -154,6 +155,10 @@ namespace {
size_t valley_capacity_prunes = 0;
size_t large_square_checks = 0;
size_t large_square_prunes = 0;
size_t component_area_checks = 0;
size_t component_area_prunes = 0;
size_t component_gcd_prunes = 0;
size_t component_subset_prunes = 0;
size_t generated_tasks = 0;
size_t completed_tasks = 0;
};
@@ -180,6 +185,24 @@ namespace {
all = 3,
};
/** Component-area rule used after a full-height boundary is created. */
enum class ComponentPruning {
disabled,
gcd,
subset_sum,
};
enum class ComponentSchedule {
boundary,
periodic_eight,
};
enum class ComponentFeasibility {
feasible,
gcd_failure,
subset_sum_failure,
};
/** Return whether a cell is the canonical representative of its D4 orbit.
*
* Reflect a cell into the left half of the board, rotate so its distance
@@ -301,13 +324,98 @@ namespace {
return true;
}
/** Return areas of empty regions separated by full-height columns.
*
* Every non-full skyline column is empty from its height to the top of the
* board. Adjacent non-full columns therefore connect at the top row, while
* a full-height column is an impassable separator.
*/
[[nodiscard]] auto empty_component_areas(
std::vector<size_t> const &skyline) -> std::vector<size_t> {
std::vector<size_t> areas;
size_t area = 0;
for (auto const height: skyline) {
if (height == skyline.size()) {
if (area != 0) {
areas.push_back(area);
area = 0;
}
} else {
area += skyline.size() - height;
}
}
if (area != 0) {
areas.push_back(area);
}
return areas;
}
/** Check necessary component-area conditions for the remaining squares.
*
* Every square lies wholly within one empty component, so each component
* area must be a sum of a bounded subset of the remaining square areas.
* Divisibility by their gcd is a cheaper necessary condition tested first.
*/
[[nodiscard]] auto component_area_feasibility(
std::vector<size_t> const &skyline, Avail const &available,
ComponentPruning const pruning) -> ComponentFeasibility {
assert(pruning != ComponentPruning::disabled);
auto const components = empty_component_areas(skyline);
if (components.size() < 2) {
return ComponentFeasibility::feasible;
}
size_t divisor = 0;
for (size_t side = 1; side < available.size(); ++side) {
if (available[side] != 0) {
divisor = std::gcd(divisor, side * side);
}
}
if (divisor != 0 &&
std::ranges::any_of(components, [divisor](size_t const area) {
return area % divisor != 0;
})) {
return ComponentFeasibility::gcd_failure;
}
if (pruning == ComponentPruning::gcd) {
return ComponentFeasibility::feasible;
}
auto const maximum_area =
*std::ranges::max_element(components);
std::vector<bool> reachable(maximum_area + 1);
reachable[0] = true;
for (size_t side = 1; side < available.size(); ++side) {
auto const square_area = side * side;
for (size_t copy = 0; copy < available[side]; ++copy) {
for (auto area = maximum_area; area >= square_area; --area) {
if (reachable[area - square_area]) {
reachable[area] = true;
}
if (area == square_area) {
break;
}
}
}
}
if (std::ranges::any_of(components, [&reachable](size_t const area) {
return !reachable[area];
})) {
return ComponentFeasibility::subset_sum_failure;
}
return ComponentFeasibility::feasible;
}
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity,
bool PruneLargeSquare>
bool PruneLargeSquare, bool PruneComponents>
auto search_skyline(size_t const n, size_t const length,
SearchPolicy const policy,
std::vector<size_t> &skyline, Avail &available,
std::vector<Square> &squares,
SearchCounters *const counters) noexcept -> bool {
SearchCounters *const counters,
ComponentPruning const component_pruning,
ComponentSchedule const component_schedule,
bool const check_components) -> bool {
if constexpr (Instrument) {
assert(counters != nullptr);
++counters->search_nodes;
@@ -317,6 +425,33 @@ namespace {
return true;
}
if constexpr (PruneComponents) {
auto const should_check =
component_schedule == ComponentSchedule::boundary
? check_components
: squares.size() % 8 == 0;
if (should_check) {
if constexpr (Instrument) {
++counters->prune_checks;
++counters->component_area_checks;
}
auto const feasibility = component_area_feasibility(
skyline, available, component_pruning);
if (feasibility != ComponentFeasibility::feasible) {
if constexpr (Instrument) {
++counters->prune_hits;
++counters->component_area_prunes;
if (feasibility == ComponentFeasibility::gcd_failure) {
++counters->component_gcd_prunes;
} else {
++counters->component_subset_prunes;
}
}
return false;
}
}
}
auto const valley = smallest_valley(skyline);
if constexpr (PruneValleyCapacity) {
if constexpr (Instrument) {
@@ -377,8 +512,10 @@ namespace {
squares.emplace_back(valley.x + valley.height * length, side);
if (search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity,
PruneLargeSquare>(
n, length, policy, skyline, available, squares, counters)) {
PruneLargeSquare, PruneComponents>(
n, length, policy, skyline, available, squares, counters,
component_pruning, component_schedule,
valley.height + side == length)) {
return true;
}
@@ -425,9 +562,11 @@ namespace {
* O(board width + n^2) local work per node and the same total state.
*/
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity,
bool PruneLargeSquare>
bool PruneLargeSquare, bool PruneComponents>
auto search_solution_impl(size_t const n, SearchPolicy const policy,
SearchCounters *const counters) noexcept
SearchCounters *const counters,
ComponentPruning const component_pruning,
ComponentSchedule const component_schedule)
-> Results {
auto const length = triangle_num(n);
std::vector<size_t> skyline(length);
@@ -439,34 +578,59 @@ namespace {
squares.reserve(length);
static_cast<void>(
search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity,
PruneLargeSquare>(
n, length, policy, skyline, available, squares, counters));
PruneLargeSquare, PruneComponents>(
n, length, policy, skyline, available, squares, counters,
component_pruning, component_schedule, false));
return {length, std::move(squares)};
}
template<bool Instrument, bool BreakD4Symmetry, bool PruneComponents>
auto search_solution_rules(size_t const n, SearchPolicy const policy,
Pruning const pruning,
SearchCounters *const counters,
ComponentPruning const component_pruning,
ComponentSchedule const component_schedule)
-> Results {
switch (pruning) {
case Pruning::all:
return search_solution_impl<Instrument, BreakD4Symmetry, true, true,
PruneComponents>(
n, policy, counters, component_pruning, component_schedule);
case Pruning::valley_capacity:
return search_solution_impl<Instrument, BreakD4Symmetry, true, false,
PruneComponents>(
n, policy, counters, component_pruning, component_schedule);
case Pruning::large_square:
return search_solution_impl<Instrument, BreakD4Symmetry, false, true,
PruneComponents>(
n, policy, counters, component_pruning, component_schedule);
case Pruning::disabled:
return search_solution_impl<Instrument, BreakD4Symmetry, false, false,
PruneComponents>(
n, policy, counters, component_pruning, component_schedule);
}
assert(false);
return search_solution_impl<Instrument, BreakD4Symmetry, false, false,
PruneComponents>(
n, policy, counters, component_pruning, component_schedule);
}
template<bool Instrument, bool BreakD4Symmetry>
auto search_solution_dispatch(size_t const n, SearchPolicy const policy,
Pruning const pruning,
SearchCounters *const counters) noexcept
SearchCounters *const counters,
ComponentPruning const component_pruning,
ComponentSchedule const component_schedule)
-> Results {
switch (pruning) {
case Pruning::all:
return search_solution_impl<Instrument, BreakD4Symmetry, true, true>(
n, policy, counters);
case Pruning::valley_capacity:
return search_solution_impl<Instrument, BreakD4Symmetry, true, false>(
n, policy, counters);
case Pruning::large_square:
return search_solution_impl<Instrument, BreakD4Symmetry, false, true>(
n, policy, counters);
case Pruning::disabled:
return search_solution_impl<Instrument, BreakD4Symmetry, false, false>(
n, policy, counters);
if (component_pruning == ComponentPruning::disabled) {
return search_solution_rules<Instrument, BreakD4Symmetry, false>(
n, policy, pruning, counters, component_pruning,
component_schedule);
}
assert(false);
return search_solution_impl<Instrument, BreakD4Symmetry, false, false>(
n, policy, counters);
return search_solution_rules<Instrument, BreakD4Symmetry, true>(
n, policy, pruning, counters, component_pruning,
component_schedule);
}
auto search_solution(
@@ -474,14 +638,20 @@ namespace {
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning = Pruning::all) noexcept
Pruning const pruning = Pruning::all,
ComponentPruning const component_pruning =
ComponentPruning::disabled,
ComponentSchedule const component_schedule =
ComponentSchedule::boundary)
-> Results {
if (symmetry == SymmetryBreaking::d4_unit_square) {
return search_solution_dispatch<false, true>(
n, policy, pruning, nullptr);
n, policy, pruning, nullptr, component_pruning,
component_schedule);
}
return search_solution_dispatch<false, false>(
n, policy, pruning, nullptr);
n, policy, pruning, nullptr, component_pruning,
component_schedule);
}
auto search_solution_instrumented(size_t const n,
@@ -491,15 +661,21 @@ namespace {
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning =
Pruning::all) noexcept
Pruning::all,
ComponentPruning const component_pruning =
ComponentPruning::disabled,
ComponentSchedule const component_schedule =
ComponentSchedule::boundary)
-> Results {
counters = {};
if (symmetry == SymmetryBreaking::d4_unit_square) {
return search_solution_dispatch<true, true>(
n, policy, pruning, &counters);
n, policy, pruning, &counters, component_pruning,
component_schedule);
}
return search_solution_dispatch<true, false>(
n, policy, pruning, &counters);
n, policy, pruning, &counters, component_pruning,
component_schedule);
}
/** Construct an odd-order solution from its even-order predecessor. */
@@ -537,12 +713,19 @@ namespace {
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning = Pruning::all) noexcept -> Results {
Pruning const pruning = Pruning::all,
ComponentPruning const component_pruning =
ComponentPruning::disabled,
ComponentSchedule const component_schedule =
ComponentSchedule::boundary) -> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(
n, search_solution(n - 1, policy, symmetry, pruning));
n, search_solution(
n - 1, policy, symmetry, pruning, component_pruning,
component_schedule));
}
return search_solution(n, policy, symmetry, pruning);
return search_solution(n, policy, symmetry, pruning, component_pruning,
component_schedule);
}
auto find_solution_instrumented(size_t const n,
@@ -552,15 +735,21 @@ namespace {
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning =
Pruning::all) noexcept
Pruning::all,
ComponentPruning const component_pruning =
ComponentPruning::disabled,
ComponentSchedule const component_schedule =
ComponentSchedule::boundary)
-> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(
n, search_solution_instrumented(
n - 1, counters, policy, symmetry, pruning));
n - 1, counters, policy, symmetry, pruning,
component_pruning, component_schedule));
}
return search_solution_instrumented(
n, counters, policy, symmetry, pruning);
n, counters, policy, symmetry, pruning, component_pruning,
component_schedule);
}
} // anon namespace