solver: prune unplaceable large squares

A skyline may retain enough total empty area while no longer containing a box for its largest remaining square. Scan for the required consecutive low columns and reject such monotonic dead states.

Keep each pruning combination independently measurable and record the small public-path gain, the direct-order-9 regression, and the rejected periodic schedule.

Tests: Debug CTest (13 passed)

Tests: ASan+UBSan CTest (13 passed)

Refs: #15
This commit was merged in pull request #27.
This commit is contained in:
Codex instance
2026-07-31 08:25:36 +01:00
parent e27427d231
commit 220cec06a9
6 changed files with 264 additions and 43 deletions
+102 -29
View File
@@ -152,6 +152,8 @@ namespace {
size_t prune_hits = 0;
size_t valley_capacity_checks = 0;
size_t valley_capacity_prunes = 0;
size_t large_square_checks = 0;
size_t large_square_prunes = 0;
size_t generated_tasks = 0;
size_t completed_tasks = 0;
};
@@ -172,8 +174,10 @@ namespace {
/** Cheap necessary conditions applied before branching at a search node. */
enum class Pruning {
disabled,
valley_capacity,
disabled = 0,
valley_capacity = 1,
large_square = 2,
all = 3,
};
/** Return whether a cell is the canonical representative of its D4 orbit.
@@ -259,7 +263,46 @@ namespace {
return available_area >= required_area;
}
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity>
/** Return whether the skyline contains an empty box for a square.
*
* A side-k square fits exactly when k consecutive columns have heights no
* greater than board height minus k. Tracking the current qualifying run
* checks this in one pass without allocations.
*/
[[nodiscard]] auto skyline_has_empty_square(
std::vector<size_t> const &skyline, size_t const side) noexcept -> bool {
assert(side > 0);
assert(side <= skyline.size());
auto const maximum_height = skyline.size() - side;
size_t run = 0;
for (auto const height: skyline) {
run = height <= maximum_height ? run + 1 : 0;
if (run == side) {
return true;
}
}
return false;
}
/** Return whether the largest remaining square has any feasible position.
*
* Feasible empty boxes are monotonic in the side length: a box which fits
* the largest remaining square also fits every smaller one. It is therefore
* sufficient to test only the largest size with non-zero multiplicity.
*/
[[nodiscard]] auto remaining_large_square_fits(
std::vector<size_t> const &skyline, Avail const &available) noexcept
-> bool {
for (auto side = available.size() - 1; side != 0; --side) {
if (available[side] != 0) {
return skyline_has_empty_square(skyline, side);
}
}
return true;
}
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity,
bool PruneLargeSquare>
auto search_skyline(size_t const n, size_t const length,
SearchPolicy const policy,
std::vector<size_t> &skyline, Avail &available,
@@ -288,6 +331,19 @@ namespace {
return false;
}
}
if constexpr (PruneLargeSquare) {
if constexpr (Instrument) {
++counters->prune_checks;
++counters->large_square_checks;
}
if (!remaining_large_square_fits(skyline, available)) {
if constexpr (Instrument) {
++counters->prune_hits;
++counters->large_square_prunes;
}
return false;
}
}
auto const largest =
std::min({n, valley.width, length - valley.height});
auto try_side = [&](size_t const side) {
@@ -320,7 +376,8 @@ namespace {
side, valley.height + side);
squares.emplace_back(valley.x + valley.height * length, side);
if (search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity>(
if (search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity,
PruneLargeSquare>(
n, length, policy, skyline, available, squares, counters)) {
return true;
}
@@ -367,7 +424,8 @@ 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, bool PruneValleyCapacity>
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity,
bool PruneLargeSquare>
auto search_solution_impl(size_t const n, SearchPolicy const policy,
SearchCounters *const counters) noexcept
-> Results {
@@ -380,29 +438,50 @@ namespace {
std::vector<Square> squares;
squares.reserve(length);
static_cast<void>(
search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity>(
n, length, policy, skyline, available, squares, counters));
search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity,
PruneLargeSquare>(
n, length, policy, skyline, available, squares, counters));
return {length, std::move(squares)};
}
template<bool Instrument, bool BreakD4Symmetry>
auto search_solution_dispatch(size_t const n, SearchPolicy const policy,
Pruning const pruning,
SearchCounters *const counters) noexcept
-> 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);
}
assert(false);
return search_solution_impl<Instrument, BreakD4Symmetry, false, false>(
n, policy, counters);
}
auto search_solution(
size_t const n,
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning = Pruning::valley_capacity) noexcept
Pruning const pruning = Pruning::all) noexcept
-> Results {
if (symmetry == SymmetryBreaking::d4_unit_square) {
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_dispatch<false, true>(
n, policy, pruning, 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);
return search_solution_dispatch<false, false>(
n, policy, pruning, nullptr);
}
auto search_solution_instrumented(size_t const n,
@@ -412,21 +491,15 @@ namespace {
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning =
Pruning::valley_capacity) noexcept
Pruning::all) noexcept
-> Results {
counters = {};
if (symmetry == SymmetryBreaking::d4_unit_square) {
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_dispatch<true, true>(
n, policy, pruning, &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);
return search_solution_dispatch<true, false>(
n, policy, pruning, &counters);
}
/** Construct an odd-order solution from its even-order predecessor. */
@@ -464,7 +537,7 @@ namespace {
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning = Pruning::valley_capacity) noexcept -> Results {
Pruning const pruning = Pruning::all) noexcept -> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(
n, search_solution(n - 1, policy, symmetry, pruning));
@@ -479,7 +552,7 @@ namespace {
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square,
Pruning const pruning =
Pruning::valley_capacity) noexcept
Pruning::all) noexcept
-> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(