solver: add optional component-area pruning #28
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user