From 39ff5cb3406448fbdbe0cb4379f124766fe6e81a Mon Sep 17 00:00:00 2001 From: Codex instance Date: Fri, 31 Jul 2026 08:40:39 +0100 Subject: [PATCH] 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 --- BENCHMARKING.md | 60 +++++++++ CMakeLists.txt | 1 + benchmarks/benchmark.cc | 69 +++++++++-- benchmarks/run.py | 33 ++++- main.cc | 263 ++++++++++++++++++++++++++++++++++------ tests/tests.cc | 118 ++++++++++++++++-- 6 files changed, 491 insertions(+), 53 deletions(-) diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 940e351..4ad8f41 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 686819b..993655c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/benchmarks/benchmark.cc b/benchmarks/benchmark.cc index b91be83..ac6a261 100644 --- a/benchmarks/benchmark.cc +++ b/benchmarks/benchmark.cc @@ -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::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; diff --git a/benchmarks/run.py b/benchmarks/run.py index d0c1aee..fe89af8 100755 --- a/benchmarks/run.py +++ b/benchmarks/run.py @@ -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, diff --git a/main.cc b/main.cc index 09429a9..c9cab8a 100644 --- a/main.cc +++ b/main.cc @@ -11,6 +11,7 @@ #include #include #include +#include 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 const &skyline) -> std::vector { + std::vector 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 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 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 PruneLargeSquare, bool PruneComponents> auto search_skyline(size_t const n, size_t const length, SearchPolicy const policy, std::vector &skyline, Avail &available, std::vector &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( - 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 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 skyline(length); @@ -439,34 +578,59 @@ namespace { squares.reserve(length); static_cast( search_skyline( - 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 + 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( + n, policy, counters, component_pruning, component_schedule); + case Pruning::valley_capacity: + return search_solution_impl( + n, policy, counters, component_pruning, component_schedule); + case Pruning::large_square: + return search_solution_impl( + n, policy, counters, component_pruning, component_schedule); + case Pruning::disabled: + return search_solution_impl( + n, policy, counters, component_pruning, component_schedule); + } + assert(false); + return search_solution_impl( + n, policy, counters, component_pruning, component_schedule); + } + template 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( - n, policy, counters); - case Pruning::valley_capacity: - return search_solution_impl( - n, policy, counters); - case Pruning::large_square: - return search_solution_impl( - n, policy, counters); - case Pruning::disabled: - return search_solution_impl( - n, policy, counters); + if (component_pruning == ComponentPruning::disabled) { + return search_solution_rules( + n, policy, pruning, counters, component_pruning, + component_schedule); } - assert(false); - return search_solution_impl( - n, policy, counters); + return search_solution_rules( + 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( - n, policy, pruning, nullptr); + n, policy, pruning, nullptr, component_pruning, + component_schedule); } return search_solution_dispatch( - 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( - n, policy, pruning, &counters); + n, policy, pruning, &counters, component_pruning, + component_schedule); } return search_solution_dispatch( - 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 diff --git a/tests/tests.cc b/tests/tests.cc index 76038cc..7716f71 100644 --- a/tests/tests.cc +++ b/tests/tests.cc @@ -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(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(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{2, 4, 1, 1}); + failures += expect( + components == std::vector{2, 6}, + "full-height boundary did not split the expected component areas"); + failures += expect( + empty_component_areas( + std::vector{2, 1, 4, 4}) == + std::vector{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{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{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{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{3, 4, 0, 4}, subset_available, + ComponentPruning::subset_sum) == + ComponentFeasibility::feasible, + "bounded subset sum rejected reachable component areas"); + + for (auto const order: std::array{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(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{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; } -- 2.54.0