solver: prune insufficient valley capacity

A selected valley cannot admit a square wider than itself until it reaches the lower neighbouring rim. Reject states whose remaining narrow-square area cannot fill that strip, including width-one and width-two gaps.

Keep an unpruned benchmark mode and dedicated counters so the rule remains independently measurable. Record the soundness argument and the measured default-on improvement.

Tests: Debug CTest (12 passed)

Tests: ASan+UBSan CTest (12 passed)

Refs: #10
This commit was merged in pull request #26.
This commit is contained in:
Codex instance
2026-07-31 08:11:30 +01:00
parent f37e08768d
commit e27427d231
6 changed files with 234 additions and 26 deletions
+87 -16
View File
@@ -150,6 +150,8 @@ namespace {
size_t backtracks = 0;
size_t prune_checks = 0;
size_t prune_hits = 0;
size_t valley_capacity_checks = 0;
size_t valley_capacity_prunes = 0;
size_t generated_tasks = 0;
size_t completed_tasks = 0;
};
@@ -168,6 +170,12 @@ namespace {
d4_unit_square,
};
/** Cheap necessary conditions applied before branching at a search node. */
enum class Pruning {
disabled,
valley_capacity,
};
/** 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
@@ -223,7 +231,35 @@ namespace {
return best;
}
template<bool Instrument, bool BreakD4Symmetry>
/** Return whether the remaining narrow pieces can fill a valley to its rim.
*
* Until the valley reaches the lower of its two neighbouring heights, no
* square wider than the valley can enter it. The area of all remaining
* squares no wider than the valley is therefore an upper bound on the area
* available to fill this isolated strip. This includes the familiar
* width-one and width-two gap checks without special cases.
*/
[[nodiscard]] auto valley_has_sufficient_capacity(
Valley const valley, std::vector<size_t> const &skyline,
Avail const &available) noexcept -> bool {
auto const end = valley.x + valley.width;
auto const left_height =
valley.x == 0 ? skyline.size() : skyline[valley.x - 1];
auto const right_height =
end == skyline.size() ? skyline.size() : skyline[end];
auto const rim_height = std::min(left_height, right_height);
auto const required_area = valley.width * (rim_height - valley.height);
size_t available_area = 0;
auto const largest = std::min(
valley.width, static_cast<size_t>(available.size() - 1));
for (size_t side = 1; side <= largest; ++side) {
available_area += available[side] * side * side;
}
return available_area >= required_area;
}
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity>
auto search_skyline(size_t const n, size_t const length,
SearchPolicy const policy,
std::vector<size_t> &skyline, Avail &available,
@@ -239,6 +275,19 @@ namespace {
}
auto const valley = smallest_valley(skyline);
if constexpr (PruneValleyCapacity) {
if constexpr (Instrument) {
++counters->prune_checks;
++counters->valley_capacity_checks;
}
if (!valley_has_sufficient_capacity(valley, skyline, available)) {
if constexpr (Instrument) {
++counters->prune_hits;
++counters->valley_capacity_prunes;
}
return false;
}
}
auto const largest =
std::min({n, valley.width, length - valley.height});
auto try_side = [&](size_t const side) {
@@ -271,7 +320,7 @@ namespace {
side, valley.height + side);
squares.emplace_back(valley.x + valley.height * length, side);
if (search_skyline<Instrument, BreakD4Symmetry>(
if (search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity>(
n, length, policy, skyline, available, squares, counters)) {
return true;
}
@@ -318,7 +367,7 @@ namespace {
* candidates and updating up to n columns for each costs O(n^2), for
* O(board width + n^2) local work per node and the same total state.
*/
template<bool Instrument, bool BreakD4Symmetry>
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity>
auto search_solution_impl(size_t const n, SearchPolicy const policy,
SearchCounters *const counters) noexcept
-> Results {
@@ -330,7 +379,8 @@ namespace {
}
std::vector<Square> squares;
squares.reserve(length);
static_cast<void>(search_skyline<Instrument, BreakD4Symmetry>(
static_cast<void>(
search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity>(
n, length, policy, skyline, available, squares, counters));
return {length, std::move(squares)};
@@ -340,12 +390,19 @@ namespace {
size_t const n,
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept
SymmetryBreaking::d4_unit_square,
Pruning const pruning = Pruning::valley_capacity) noexcept
-> Results {
if (symmetry == SymmetryBreaking::d4_unit_square) {
return search_solution_impl<false, true>(n, policy, nullptr);
if (pruning == Pruning::valley_capacity) {
return search_solution_impl<false, true, true>(n, policy, nullptr);
}
return search_solution_impl<false, true, false>(n, policy, nullptr);
}
return search_solution_impl<false, false>(n, policy, nullptr);
if (pruning == Pruning::valley_capacity) {
return search_solution_impl<false, false, true>(n, policy, nullptr);
}
return search_solution_impl<false, false, false>(n, policy, nullptr);
}
auto search_solution_instrumented(size_t const n,
@@ -353,13 +410,23 @@ namespace {
SearchPolicy const policy =
SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept
SymmetryBreaking::d4_unit_square,
Pruning const pruning =
Pruning::valley_capacity) noexcept
-> Results {
counters = {};
if (symmetry == SymmetryBreaking::d4_unit_square) {
return search_solution_impl<true, true>(n, policy, &counters);
if (pruning == Pruning::valley_capacity) {
return search_solution_impl<true, true, true>(
n, policy, &counters);
}
return search_solution_impl<true, true, false>(n, policy, &counters);
}
return search_solution_impl<true, false>(n, policy, &counters);
if (pruning == Pruning::valley_capacity) {
return search_solution_impl<true, false, true>(
n, policy, &counters);
}
return search_solution_impl<true, false, false>(n, policy, &counters);
}
/** Construct an odd-order solution from its even-order predecessor. */
@@ -396,12 +463,13 @@ namespace {
size_t const n,
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept -> Results {
SymmetryBreaking::d4_unit_square,
Pruning const pruning = Pruning::valley_capacity) noexcept -> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(
n, search_solution(n - 1, policy, symmetry));
n, search_solution(n - 1, policy, symmetry, pruning));
}
return search_solution(n, policy, symmetry);
return search_solution(n, policy, symmetry, pruning);
}
auto find_solution_instrumented(size_t const n,
@@ -409,14 +477,17 @@ namespace {
SearchPolicy const policy =
SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept
SymmetryBreaking::d4_unit_square,
Pruning const pruning =
Pruning::valley_capacity) noexcept
-> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(
n, search_solution_instrumented(
n - 1, counters, policy, symmetry));
n - 1, counters, policy, symmetry, pruning));
}
return search_solution_instrumented(n, counters, policy, symmetry);
return search_solution_instrumented(
n, counters, policy, symmetry, pruning);
}
} // anon namespace