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