solver: add optional component-area pruning #28

Merged
mcp merged 1 commits from codex/issue-13-component-area into main 2026-07-31 08:41:16 +01:00
6 changed files with 491 additions and 53 deletions
Showing only changes of commit 39ff5cb340 - Show all commits
+60
View File
@@ -24,6 +24,10 @@ otherwise identical unconstrained baseline.
The production default also applies the pruning rules described below.
Use `--pruning valley-capacity`, `--pruning large-square`, or
`--no-pruning` to measure each rule alone or obtain an unpruned search.
Use `--component-pruning gcd`, `--component-pruning subset-sum`, or
`--component-pruning none` to compare the component-area rule separately.
Component pruning is disabled by default because the measurements below do not
recover its cost.
Order 9 uses the constructive odd-order path, searching order 8 and then tiling
the enlarged border, so it is suitable for normal local benchmarking:
@@ -157,6 +161,62 @@ is already recovered; it was not added without evidence that the complexity
would improve elapsed time. The simple every-node scan remains enabled by
default for the measured public benchmark benefit.
## Component-area pruning
In a skyline, every non-full column is empty from its filled height to the top
of the board. Adjacent non-full columns therefore belong to the same empty
component, while a full-height column is an impassable separator. Each
remaining square must lie wholly within one such component, so every component
area must be the sum of a bounded subset of the remaining square areas.
The `gcd` mode first rejects a component area which is not divisible by the
greatest common divisor of all remaining square areas. The `subset-sum` mode
then computes exact reachable areas using each remaining multiplicity as a
bound. Each component is checked against the same reachable set; this is a
necessary condition, not a claim that independently selected subsets are
mutually disjoint.
The check is triggered only after a placement reaches full board height and
can create or extend a component boundary. This keeps the potentially more
expensive bounded subset sum off ordinary nodes. Instrumented output records
`component_area_checks`, `component_area_prunes`, `component_gcd_prunes`, and
`component_subset_prunes`. The three `--component-pruning` modes allow the
trigger cost, cheap gcd rule, and bounded subset sum to be compared directly.
Use `--component-schedule periodic-8` to compare the event-driven boundary
trigger with checking every eighth placement depth.
Measurements used the issue #13 working tree based on commit `220cec0`, Apple
Clang 21.0.0, `-O3 -DNDEBUG`, macOS arm64, one worker, one warm-up, and seven
measured repetitions for the boundary-trigger modes. Counter-free and counted
runs were interleaved; the table reports counter-free medians. The existing
valley-capacity and large-square rules remained enabled. All results passed
independent validation and counters were stable:
| Order | Component rule | Median solve | Nodes | Checks | Prunes |
| --- | --- | ---: | ---: | ---: | ---: |
| 7 exhaustive | disabled | 0.985 s | 13,221,239 | 0 | 0 |
| 7 exhaustive | gcd | 1.027 s | 13,220,729 | 171,088 | 2,568 |
| 7 exhaustive | subset sum | 1.052 s | 13,189,961 | 161,706 | 69,483 |
| 8 first solution | disabled | 0.203 s | 2,597,678 | 0 | 0 |
| 8 first solution | gcd | 0.212 s | 2,597,548 | 23,943 | 637 |
| 8 first solution | subset sum | 0.219 s | 2,592,212 | 23,097 | 11,858 |
Gcd-only checking changed fewer than 0.005% of nodes while slowing the
counter-free solver by 4.3% for both orders. Bounded subset sum reduced nodes
by only 0.24% for order 7 and 0.21% for order 8, while slowing them by 6.8% and
7.5%. Neither rule is enabled by default.
The boundary trigger is an incremental event check: it runs only when the
latest placement reaches full height and can change the component partition.
For comparison, subset sum was also sampled every eighth placement depth with
one warm-up and three measured repetitions. Periodic checking made 1,501,035
checks for order 7 and 290,774 for order 8, versus 161,706 and 23,097 at
boundary events. Its counter-free medians were 1.088 s and 0.227 s, 10.5% and
11.7% slower than disabled pruning and materially worse than boundary
triggering. The periodic schedule is retained only as an opt-in measurement
mode; event-triggered checking is the cheaper schedule if component pruning is
reconsidered with new evidence.
## D4 board symmetry
Every solution contains exactly one 1-by-1 square. Rotations and reflections
+1
View File
@@ -28,6 +28,7 @@ if(BUILD_TESTING)
add_test(NAME d4-symmetry COMMAND partridge_tests d4-symmetry)
add_test(NAME valley-capacity COMMAND partridge_tests valley-capacity)
add_test(NAME large-square COMMAND partridge_tests large-square)
add_test(NAME component-area COMMAND partridge_tests component-area)
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME benchmark-format
+62 -7
View File
@@ -30,6 +30,8 @@ namespace {
SearchPolicy policy, bool direct_search,
SymmetryBreaking symmetry,
Pruning pruning,
ComponentPruning component_pruning,
ComponentSchedule component_schedule,
SearchCounters &counters)
-> TimedSolution {
auto const predecessor_order =
@@ -38,8 +40,11 @@ namespace {
auto predecessor =
instrument
? search_solution_instrumented(predecessor_order, counters,
policy, symmetry, pruning)
: search_solution(predecessor_order, policy, symmetry, pruning);
policy, symmetry, pruning,
component_pruning,
component_schedule)
: search_solution(predecessor_order, policy, symmetry, pruning,
component_pruning, component_schedule);
auto const search_end = Clock::now();
auto const construction_begin = Clock::now();
@@ -103,13 +108,30 @@ namespace {
assert(false);
return "none";
}
auto component_pruning_name(ComponentPruning const pruning) -> char const * {
switch (pruning) {
case ComponentPruning::disabled: return "none";
case ComponentPruning::gcd: return "gcd";
case ComponentPruning::subset_sum: return "subset-sum";
}
assert(false);
return "none";
}
auto component_schedule_name(ComponentSchedule const schedule)
-> char const * {
return schedule == ComponentSchedule::boundary ? "boundary"
: "periodic-8";
}
}
int main(int argc, char **argv) {
if (argc < 3 || argc > 7) {
if (argc < 3 || argc > 9) {
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
"[ascending|descending|best-fit] [public|direct] "
"[d4|none] [all|valley-capacity|large-square|none]\n";
"[d4|none] [all|valley-capacity|large-square|none] "
"[subset-sum|gcd|none] [boundary|periodic-8]\n";
return 2;
}
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
@@ -141,7 +163,7 @@ int main(int argc, char **argv) {
argc < 6 || std::string_view(argv[5]) == "d4"
? SymmetryBreaking::d4_unit_square
: SymmetryBreaking::disabled;
if (argc == 6 && std::string_view(argv[5]) != "d4" &&
if (argc >= 6 && std::string_view(argv[5]) != "d4" &&
std::string_view(argv[5]) != "none") {
std::cerr << "symmetry mode must be d4 or none\n";
return 2;
@@ -154,7 +176,7 @@ int main(int argc, char **argv) {
: std::string_view(argv[6]) == "large-square"
? Pruning::large_square
: Pruning::disabled;
if (argc == 7 && std::string_view(argv[6]) != "all" &&
if (argc >= 7 && std::string_view(argv[6]) != "all" &&
std::string_view(argv[6]) != "valley-capacity" &&
std::string_view(argv[6]) != "large-square" &&
std::string_view(argv[6]) != "none") {
@@ -162,11 +184,32 @@ int main(int argc, char **argv) {
"large-square, or none\n";
return 2;
}
auto const component_pruning =
argc < 8 || std::string_view(argv[7]) == "none"
? ComponentPruning::disabled
: std::string_view(argv[7]) == "gcd"
? ComponentPruning::gcd
: ComponentPruning::subset_sum;
if (argc >= 8 && std::string_view(argv[7]) != "subset-sum" &&
std::string_view(argv[7]) != "gcd" &&
std::string_view(argv[7]) != "none") {
std::cerr << "component pruning mode must be subset-sum, gcd, or none\n";
return 2;
}
auto const component_schedule =
argc < 9 || std::string_view(argv[8]) == "boundary"
? ComponentSchedule::boundary
: ComponentSchedule::periodic_eight;
if (argc == 9 && std::string_view(argv[8]) != "boundary" &&
std::string_view(argv[8]) != "periodic-8") {
std::cerr << "component schedule must be boundary or periodic-8\n";
return 2;
}
SearchCounters counters;
auto timed =
solve(order, instrument, policy, direct_search, symmetry, pruning,
counters);
component_pruning, component_schedule, counters);
auto const validation_begin = Clock::now();
auto const validation_ok = valid(order, timed.result);
@@ -196,6 +239,10 @@ int main(int argc, char **argv) {
<< "\""
<< ",\"pruning\":\""
<< pruning_name(pruning) << "\""
<< ",\"component_pruning\":\""
<< component_pruning_name(component_pruning) << "\""
<< ",\"component_schedule\":\""
<< component_schedule_name(component_schedule) << "\""
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
<< ",\"valid\":" << boolean(validation_ok)
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
@@ -216,6 +263,14 @@ int main(int argc, char **argv) {
<< counters.large_square_checks
<< ",\"large_square_prunes\":"
<< counters.large_square_prunes
<< ",\"component_area_checks\":"
<< counters.component_area_checks
<< ",\"component_area_prunes\":"
<< counters.component_area_prunes
<< ",\"component_gcd_prunes\":"
<< counters.component_gcd_prunes
<< ",\"component_subset_prunes\":"
<< counters.component_subset_prunes
<< ",\"generated_tasks\":" << counters.generated_tasks
<< ",\"completed_tasks\":" << counters.completed_tasks << "}}\n";
return validation_ok ? 0 : 1;
+32 -1
View File
@@ -80,7 +80,16 @@ def environment(binary):
def run_once(
binary, order, mode, search_policy, search_route, symmetry, pruning, timeout
binary,
order,
mode,
search_policy,
search_route,
symmetry,
pruning,
component_pruning,
component_schedule,
timeout,
):
started = time.monotonic()
try:
@@ -93,6 +102,8 @@ def run_once(
search_route,
symmetry,
pruning,
component_pruning,
component_schedule,
],
check=False,
capture_output=True,
@@ -249,6 +260,18 @@ def main():
action="store_true",
help="disable all pruning (compatibility alias for --pruning none)",
)
parser.add_argument(
"--component-pruning",
choices=("subset-sum", "gcd", "none"),
default="none",
help="select component-area pruning strength",
)
parser.add_argument(
"--component-schedule",
choices=("boundary", "periodic-8"),
default="boundary",
help="trigger component checks on boundaries or every eighth depth",
)
parser.add_argument(
"--measure-overhead",
action="store_true",
@@ -282,6 +305,8 @@ def main():
"direct" if args.direct_search else "public",
"none" if args.no_symmetry else "d4",
"none" if args.no_pruning else args.pruning,
"none" if args.no_pruning else args.component_pruning,
args.component_schedule,
args.timeout,
)
for trial in range(args.repetitions):
@@ -295,6 +320,8 @@ def main():
"direct" if args.direct_search else "public",
"none" if args.no_symmetry else "d4",
"none" if args.no_pruning else args.pruning,
"none" if args.no_pruning else args.component_pruning,
args.component_schedule,
args.timeout,
)
)
@@ -325,6 +352,10 @@ def main():
"search_route": "direct" if args.direct_search else "public",
"symmetry_breaking": "none" if args.no_symmetry else "d4",
"pruning": "none" if args.no_pruning else args.pruning,
"component_pruning": (
"none" if args.no_pruning else args.component_pruning
),
"component_schedule": args.component_schedule,
"stdout": "captured; rendered grid suppressed by probe",
},
"cases": cases,
+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
+110 -8
View File
@@ -371,6 +371,11 @@ namespace {
counters.valley_capacity_prunes &&
counters.large_square_checks >=
counters.large_square_prunes &&
counters.component_area_checks >=
counters.component_area_prunes &&
counters.component_area_prunes ==
counters.component_gcd_prunes +
counters.component_subset_prunes &&
counters.generated_tasks == 0 &&
counters.completed_tasks == 0,
"search counters are inconsistent");
@@ -431,10 +436,12 @@ namespace {
SearchCounters disabled_counters;
auto const enabled = search_solution_instrumented(
8, enabled_counters, SearchPolicy::ascending,
SymmetryBreaking::d4_unit_square, Pruning::disabled);
SymmetryBreaking::d4_unit_square, Pruning::disabled,
ComponentPruning::disabled);
auto const disabled = search_solution_instrumented(
8, disabled_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::disabled);
SymmetryBreaking::disabled, Pruning::disabled,
ComponentPruning::disabled);
auto enabled_validation = validate(8, enabled);
auto disabled_validation = validate(8, disabled);
failures += expect(
@@ -557,10 +564,12 @@ namespace {
SearchCounters unpruned_counters;
auto const pruned = search_solution_instrumented(
order, pruned_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::valley_capacity);
SymmetryBreaking::disabled, Pruning::valley_capacity,
ComponentPruning::disabled);
auto const unpruned = search_solution_instrumented(
order, unpruned_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::disabled);
SymmetryBreaking::disabled, Pruning::disabled,
ComponentPruning::disabled);
failures += expect(
pruned.squares().empty() == unpruned.squares().empty(),
"valley pruning changed exhaustive order-" +
@@ -574,7 +583,7 @@ namespace {
SearchCounters counters;
static_cast<void>(search_solution_instrumented(
5, counters, SearchPolicy::ascending, SymmetryBreaking::disabled,
Pruning::valley_capacity));
Pruning::valley_capacity, ComponentPruning::disabled));
failures += expect(
counters.valley_capacity_checks > 0 &&
counters.valley_capacity_prunes > 0 &&
@@ -622,10 +631,12 @@ namespace {
SearchCounters baseline_counters;
auto const pruned = search_solution_instrumented(
order, pruned_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::all);
SymmetryBreaking::disabled, Pruning::all,
ComponentPruning::disabled);
auto const baseline = search_solution_instrumented(
order, baseline_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::valley_capacity);
SymmetryBreaking::disabled, Pruning::valley_capacity,
ComponentPruning::disabled);
failures += expect(
pruned.squares().empty() == baseline.squares().empty(),
"large-square pruning changed exhaustive order-" +
@@ -639,7 +650,7 @@ namespace {
SearchCounters counters;
static_cast<void>(search_solution_instrumented(
5, counters, SearchPolicy::ascending, SymmetryBreaking::disabled,
Pruning::large_square));
Pruning::large_square, ComponentPruning::disabled));
failures += expect(
counters.large_square_checks > 0 &&
counters.large_square_prunes > 0 &&
@@ -649,6 +660,94 @@ namespace {
return failures;
}
auto test_component_area_pruning() -> int {
int failures = 0;
auto const components =
empty_component_areas(std::vector<std::uint64_t>{2, 4, 1, 1});
failures += expect(
components == std::vector<std::uint64_t>{2, 6},
"full-height boundary did not split the expected component areas");
failures += expect(
empty_component_areas(
std::vector<std::uint64_t>{2, 1, 4, 4}) ==
std::vector<std::uint64_t>{5},
"adjacent non-full columns did not remain one empty component");
Avail gcd_available(3);
gcd_available[2] = 2;
failures += expect(
component_area_feasibility(
std::vector<std::uint64_t>{2, 4, 1, 1}, gcd_available,
ComponentPruning::gcd) ==
ComponentFeasibility::gcd_failure,
"component gcd check accepted indivisible component areas");
Avail subset_available(3);
subset_available[1] = 1;
subset_available[2] = 1;
failures += expect(
component_area_feasibility(
std::vector<std::uint64_t>{2, 4, 1, 4}, subset_available,
ComponentPruning::gcd) ==
ComponentFeasibility::feasible,
"component gcd check rejected a gcd-one state");
failures += expect(
component_area_feasibility(
std::vector<std::uint64_t>{2, 4, 1, 4}, subset_available,
ComponentPruning::subset_sum) ==
ComponentFeasibility::subset_sum_failure,
"bounded subset sum accepted unreachable component areas");
failures += expect(
component_area_feasibility(
std::vector<std::uint64_t>{3, 4, 0, 4}, subset_available,
ComponentPruning::subset_sum) ==
ComponentFeasibility::feasible,
"bounded subset sum rejected reachable component areas");
for (auto const order: std::array<std::uint64_t, 5>{1, 2, 3, 4, 5}) {
SearchCounters pruned_counters;
SearchCounters periodic_counters;
SearchCounters baseline_counters;
auto const pruned = search_solution_instrumented(
order, pruned_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::all,
ComponentPruning::subset_sum);
auto const periodic = search_solution_instrumented(
order, periodic_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::all,
ComponentPruning::subset_sum,
ComponentSchedule::periodic_eight);
auto const baseline = search_solution_instrumented(
order, baseline_counters, SearchPolicy::ascending,
SymmetryBreaking::disabled, Pruning::all,
ComponentPruning::disabled);
failures += expect(
pruned.squares().empty() == baseline.squares().empty() &&
periodic.squares().empty() == baseline.squares().empty(),
"component pruning changed exhaustive order-" +
std::to_string(order) + " feasibility");
failures += expect(
pruned_counters.search_nodes <= baseline_counters.search_nodes &&
periodic_counters.search_nodes <=
baseline_counters.search_nodes,
"component pruning enlarged the exhaustive order-" +
std::to_string(order) + " search");
}
SearchCounters counters;
static_cast<void>(search_solution_instrumented(
5, counters, SearchPolicy::ascending, SymmetryBreaking::disabled,
Pruning::disabled, ComponentPruning::subset_sum));
failures += expect(
counters.component_area_checks > 0 &&
counters.component_area_prunes > 0 &&
counters.component_area_prunes ==
counters.component_gcd_prunes +
counters.component_subset_prunes,
"component-area instrumentation did not count checks and prune kinds");
return failures;
}
auto test_solver_completion() -> int {
int failures = 0;
for (auto const order: std::array<std::uint64_t, 2>{1, 8}) {
@@ -703,6 +802,9 @@ int main(int argc, char **argv) {
if (test == "large-square") {
return test_large_square_pruning();
}
if (test == "component-area") {
return test_component_area_pruning();
}
std::cerr << "unknown test: " << test << '\n';
return 2;
}