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
+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;
}