solver: prune unplaceable large squares
A skyline may retain enough total empty area while no longer containing a box for its largest remaining square. Scan for the required consecutive low columns and reject such monotonic dead states. Keep each pruning combination independently measurable and record the small public-path gain, the direct-order-9 regression, and the rejected periodic schedule. Tests: Debug CTest (13 passed) Tests: ASan+UBSan CTest (13 passed) Refs: #15
This commit was merged in pull request #27.
This commit is contained in:
+52
-2
@@ -21,8 +21,9 @@ and `--candidate-order`. The choices are `ascending`, `descending`, and
|
|||||||
The production default also removes equivalent D4 board orientations by
|
The production default also removes equivalent D4 board orientations by
|
||||||
constraining the unique unit square. Pass `--no-symmetry` to obtain an
|
constraining the unique unit square. Pass `--no-symmetry` to obtain an
|
||||||
otherwise identical unconstrained baseline.
|
otherwise identical unconstrained baseline.
|
||||||
The production default also applies the valley-capacity rule described below.
|
The production default also applies the pruning rules described below.
|
||||||
Pass `--no-pruning` to obtain an otherwise identical unpruned search.
|
Use `--pruning valley-capacity`, `--pruning large-square`, or
|
||||||
|
`--no-pruning` to measure each rule alone or obtain an unpruned search.
|
||||||
Order 9 uses the constructive odd-order path, searching order 8 and then tiling
|
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:
|
the enlarged border, so it is suitable for normal local benchmarking:
|
||||||
|
|
||||||
@@ -107,6 +108,55 @@ The rule reduced nodes by 7.8% for order 7 and 7.1% for order 8. Its low
|
|||||||
per-node cost also reduced median counted solve time by 10.3% and 9.9%,
|
per-node cost also reduced median counted solve time by 10.3% and 9.9%,
|
||||||
respectively, so it remains enabled by default.
|
respectively, so it remains enabled by default.
|
||||||
|
|
||||||
|
## Remaining-large-square pruning
|
||||||
|
|
||||||
|
For a remaining side `k`, a skyline can contain an empty `k`-by-`k` box only
|
||||||
|
if it has `k` consecutive columns whose filled heights are no greater than the
|
||||||
|
board height minus `k`. A single linear scan tracks qualifying consecutive
|
||||||
|
columns. If no such run exists, the square cannot be placed and the state is
|
||||||
|
infeasible even when its total empty area is sufficient.
|
||||||
|
|
||||||
|
Empty-box feasibility is monotonic in the side length: a box which fits the
|
||||||
|
largest remaining square also fits every smaller remaining size. The solver
|
||||||
|
therefore invokes only one `O(board width)` scan per surviving node, after the
|
||||||
|
cheaper valley-capacity check. It does not attempt an unproven multiplicity
|
||||||
|
bound. `--pruning large-square` measures the rule independently;
|
||||||
|
`--pruning valley-capacity` provides the production baseline without it.
|
||||||
|
Instrumented output records `large_square_checks` and
|
||||||
|
`large_square_prunes`.
|
||||||
|
|
||||||
|
Measurements used the issue #15 working tree based on commit `e27427d`, Apple
|
||||||
|
Clang 21.0.0, `-O3 -DNDEBUG`, macOS arm64, one worker, one warm-up, and seven
|
||||||
|
measured repetitions. Counter-free and counted runs were interleaved; the
|
||||||
|
table reports the counter-free production instantiation. The baseline keeps
|
||||||
|
valley-capacity pruning enabled, so it isolates the new rule. All results
|
||||||
|
passed independent validation and counters were stable:
|
||||||
|
|
||||||
|
| Search | Large-square check | Median solve | Nodes | Checks | Prunes |
|
||||||
|
| --- | --- | ---: | ---: | ---: | ---: |
|
||||||
|
| Order 7 exhaustive | enabled | 0.946 s | 13,221,239 | 6,163,390 | 227,321 |
|
||||||
|
| Order 7 exhaustive | disabled | 0.947 s | 13,833,048 | 0 | 0 |
|
||||||
|
| Order 8 first solution | enabled | 0.195 s | 2,597,678 | 1,063,472 | 38,177 |
|
||||||
|
| Order 8 first solution | disabled | 0.197 s | 2,724,096 | 0 | 0 |
|
||||||
|
| Order 9 direct | enabled | 1.225 s | 14,840,146 | 5,592,843 | 54,900 |
|
||||||
|
| Order 9 direct | disabled | 1.208 s | 14,995,127 | 0 | 0 |
|
||||||
|
|
||||||
|
The rule reduced nodes by 4.4% for order 7, 4.6% for order 8, and 1.0% for
|
||||||
|
direct order 9. Counter-free median time improved by 0.12% and 0.89% on the
|
||||||
|
two distinct searches in the public benchmark set. Public order 9 constructs
|
||||||
|
from the improving order-8 search. Direct order 9, which bypasses that public
|
||||||
|
route, regressed by 1.39%; it remains documented as a caution for future
|
||||||
|
policy tuning.
|
||||||
|
|
||||||
|
Checking only every fourth placement depth was also measured. It retained
|
||||||
|
fewer prunes and was slower than checking every surviving node by 2.2% for
|
||||||
|
order 7, 2.3% for order 8, and 0.6% for direct order 9, so the periodic
|
||||||
|
schedule was rejected. Incremental maintenance would need additional
|
||||||
|
per-size window state and undo logic for an `O(W)` scan whose public net cost
|
||||||
|
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.
|
||||||
|
|
||||||
## D4 board symmetry
|
## D4 board symmetry
|
||||||
|
|
||||||
Every solution contains exactly one 1-by-1 square. Rotations and reflections
|
Every solution contains exactly one 1-by-1 square. Rotations and reflections
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ if(BUILD_TESTING)
|
|||||||
add_test(NAME skyline-search COMMAND partridge_tests skyline-search)
|
add_test(NAME skyline-search COMMAND partridge_tests skyline-search)
|
||||||
add_test(NAME d4-symmetry COMMAND partridge_tests d4-symmetry)
|
add_test(NAME d4-symmetry COMMAND partridge_tests d4-symmetry)
|
||||||
add_test(NAME valley-capacity COMMAND partridge_tests valley-capacity)
|
add_test(NAME valley-capacity COMMAND partridge_tests valley-capacity)
|
||||||
|
add_test(NAME large-square COMMAND partridge_tests large-square)
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
if(Python3_Interpreter_FOUND)
|
if(Python3_Interpreter_FOUND)
|
||||||
add_test(NAME benchmark-format
|
add_test(NAME benchmark-format
|
||||||
|
|||||||
+27
-6
@@ -92,13 +92,24 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto boolean(bool value) -> char const * { return value ? "true" : "false"; }
|
auto boolean(bool value) -> char const * { return value ? "true" : "false"; }
|
||||||
|
|
||||||
|
auto pruning_name(Pruning const pruning) -> char const * {
|
||||||
|
switch (pruning) {
|
||||||
|
case Pruning::all: return "all";
|
||||||
|
case Pruning::valley_capacity: return "valley-capacity";
|
||||||
|
case Pruning::large_square: return "large-square";
|
||||||
|
case Pruning::disabled: return "none";
|
||||||
|
}
|
||||||
|
assert(false);
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
if (argc < 3 || argc > 7) {
|
if (argc < 3 || argc > 7) {
|
||||||
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
|
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
|
||||||
"[ascending|descending|best-fit] [public|direct] "
|
"[ascending|descending|best-fit] [public|direct] "
|
||||||
"[d4|none] [valley-capacity|none]\n";
|
"[d4|none] [all|valley-capacity|large-square|none]\n";
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
|
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
|
||||||
@@ -136,12 +147,19 @@ int main(int argc, char **argv) {
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
auto const pruning =
|
auto const pruning =
|
||||||
argc < 7 || std::string_view(argv[6]) == "valley-capacity"
|
argc < 7 || std::string_view(argv[6]) == "all"
|
||||||
|
? Pruning::all
|
||||||
|
: std::string_view(argv[6]) == "valley-capacity"
|
||||||
? Pruning::valley_capacity
|
? Pruning::valley_capacity
|
||||||
|
: std::string_view(argv[6]) == "large-square"
|
||||||
|
? Pruning::large_square
|
||||||
: Pruning::disabled;
|
: Pruning::disabled;
|
||||||
if (argc == 7 && std::string_view(argv[6]) != "valley-capacity" &&
|
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") {
|
std::string_view(argv[6]) != "none") {
|
||||||
std::cerr << "pruning mode must be valley-capacity or none\n";
|
std::cerr << "pruning mode must be all, valley-capacity, "
|
||||||
|
"large-square, or none\n";
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,8 +195,7 @@ int main(int argc, char **argv) {
|
|||||||
<< (symmetry == SymmetryBreaking::d4_unit_square ? "d4" : "none")
|
<< (symmetry == SymmetryBreaking::d4_unit_square ? "d4" : "none")
|
||||||
<< "\""
|
<< "\""
|
||||||
<< ",\"pruning\":\""
|
<< ",\"pruning\":\""
|
||||||
<< (pruning == Pruning::valley_capacity ? "valley-capacity" : "none")
|
<< pruning_name(pruning) << "\""
|
||||||
<< "\""
|
|
||||||
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
|
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
|
||||||
<< ",\"valid\":" << boolean(validation_ok)
|
<< ",\"valid\":" << boolean(validation_ok)
|
||||||
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
|
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
|
||||||
@@ -195,6 +212,10 @@ int main(int argc, char **argv) {
|
|||||||
<< counters.valley_capacity_checks
|
<< counters.valley_capacity_checks
|
||||||
<< ",\"valley_capacity_prunes\":"
|
<< ",\"valley_capacity_prunes\":"
|
||||||
<< counters.valley_capacity_prunes
|
<< counters.valley_capacity_prunes
|
||||||
|
<< ",\"large_square_checks\":"
|
||||||
|
<< counters.large_square_checks
|
||||||
|
<< ",\"large_square_prunes\":"
|
||||||
|
<< counters.large_square_prunes
|
||||||
<< ",\"generated_tasks\":" << counters.generated_tasks
|
<< ",\"generated_tasks\":" << counters.generated_tasks
|
||||||
<< ",\"completed_tasks\":" << counters.completed_tasks << "}}\n";
|
<< ",\"completed_tasks\":" << counters.completed_tasks << "}}\n";
|
||||||
return validation_ok ? 0 : 1;
|
return validation_ok ? 0 : 1;
|
||||||
|
|||||||
+10
-4
@@ -238,10 +238,16 @@ def main():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="disable unique-unit-square D4 symmetry breaking",
|
help="disable unique-unit-square D4 symmetry breaking",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pruning",
|
||||||
|
choices=("all", "valley-capacity", "large-square", "none"),
|
||||||
|
default="all",
|
||||||
|
help="select independently measurable pruning rules",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-pruning",
|
"--no-pruning",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="disable valley-capacity pruning",
|
help="disable all pruning (compatibility alias for --pruning none)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--measure-overhead",
|
"--measure-overhead",
|
||||||
@@ -275,7 +281,7 @@ def main():
|
|||||||
args.search_policy,
|
args.search_policy,
|
||||||
"direct" if args.direct_search else "public",
|
"direct" if args.direct_search else "public",
|
||||||
"none" if args.no_symmetry else "d4",
|
"none" if args.no_symmetry else "d4",
|
||||||
"none" if args.no_pruning else "valley-capacity",
|
"none" if args.no_pruning else args.pruning,
|
||||||
args.timeout,
|
args.timeout,
|
||||||
)
|
)
|
||||||
for trial in range(args.repetitions):
|
for trial in range(args.repetitions):
|
||||||
@@ -288,7 +294,7 @@ def main():
|
|||||||
args.search_policy,
|
args.search_policy,
|
||||||
"direct" if args.direct_search else "public",
|
"direct" if args.direct_search else "public",
|
||||||
"none" if args.no_symmetry else "d4",
|
"none" if args.no_symmetry else "d4",
|
||||||
"none" if args.no_pruning else "valley-capacity",
|
"none" if args.no_pruning else args.pruning,
|
||||||
args.timeout,
|
args.timeout,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -318,7 +324,7 @@ def main():
|
|||||||
"candidate_order": args.search_policy,
|
"candidate_order": args.search_policy,
|
||||||
"search_route": "direct" if args.direct_search else "public",
|
"search_route": "direct" if args.direct_search else "public",
|
||||||
"symmetry_breaking": "none" if args.no_symmetry else "d4",
|
"symmetry_breaking": "none" if args.no_symmetry else "d4",
|
||||||
"pruning": "none" if args.no_pruning else "valley-capacity",
|
"pruning": "none" if args.no_pruning else args.pruning,
|
||||||
"stdout": "captured; rendered grid suppressed by probe",
|
"stdout": "captured; rendered grid suppressed by probe",
|
||||||
},
|
},
|
||||||
"cases": cases,
|
"cases": cases,
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ namespace {
|
|||||||
size_t prune_hits = 0;
|
size_t prune_hits = 0;
|
||||||
size_t valley_capacity_checks = 0;
|
size_t valley_capacity_checks = 0;
|
||||||
size_t valley_capacity_prunes = 0;
|
size_t valley_capacity_prunes = 0;
|
||||||
|
size_t large_square_checks = 0;
|
||||||
|
size_t large_square_prunes = 0;
|
||||||
size_t generated_tasks = 0;
|
size_t generated_tasks = 0;
|
||||||
size_t completed_tasks = 0;
|
size_t completed_tasks = 0;
|
||||||
};
|
};
|
||||||
@@ -172,8 +174,10 @@ namespace {
|
|||||||
|
|
||||||
/** Cheap necessary conditions applied before branching at a search node. */
|
/** Cheap necessary conditions applied before branching at a search node. */
|
||||||
enum class Pruning {
|
enum class Pruning {
|
||||||
disabled,
|
disabled = 0,
|
||||||
valley_capacity,
|
valley_capacity = 1,
|
||||||
|
large_square = 2,
|
||||||
|
all = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Return whether a cell is the canonical representative of its D4 orbit.
|
/** Return whether a cell is the canonical representative of its D4 orbit.
|
||||||
@@ -259,7 +263,46 @@ namespace {
|
|||||||
return available_area >= required_area;
|
return available_area >= required_area;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity>
|
/** Return whether the skyline contains an empty box for a square.
|
||||||
|
*
|
||||||
|
* A side-k square fits exactly when k consecutive columns have heights no
|
||||||
|
* greater than board height minus k. Tracking the current qualifying run
|
||||||
|
* checks this in one pass without allocations.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] auto skyline_has_empty_square(
|
||||||
|
std::vector<size_t> const &skyline, size_t const side) noexcept -> bool {
|
||||||
|
assert(side > 0);
|
||||||
|
assert(side <= skyline.size());
|
||||||
|
auto const maximum_height = skyline.size() - side;
|
||||||
|
size_t run = 0;
|
||||||
|
for (auto const height: skyline) {
|
||||||
|
run = height <= maximum_height ? run + 1 : 0;
|
||||||
|
if (run == side) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return whether the largest remaining square has any feasible position.
|
||||||
|
*
|
||||||
|
* Feasible empty boxes are monotonic in the side length: a box which fits
|
||||||
|
* the largest remaining square also fits every smaller one. It is therefore
|
||||||
|
* sufficient to test only the largest size with non-zero multiplicity.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] auto remaining_large_square_fits(
|
||||||
|
std::vector<size_t> const &skyline, Avail const &available) noexcept
|
||||||
|
-> bool {
|
||||||
|
for (auto side = available.size() - 1; side != 0; --side) {
|
||||||
|
if (available[side] != 0) {
|
||||||
|
return skyline_has_empty_square(skyline, side);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity,
|
||||||
|
bool PruneLargeSquare>
|
||||||
auto search_skyline(size_t const n, size_t const length,
|
auto search_skyline(size_t const n, size_t const length,
|
||||||
SearchPolicy const policy,
|
SearchPolicy const policy,
|
||||||
std::vector<size_t> &skyline, Avail &available,
|
std::vector<size_t> &skyline, Avail &available,
|
||||||
@@ -288,6 +331,19 @@ namespace {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if constexpr (PruneLargeSquare) {
|
||||||
|
if constexpr (Instrument) {
|
||||||
|
++counters->prune_checks;
|
||||||
|
++counters->large_square_checks;
|
||||||
|
}
|
||||||
|
if (!remaining_large_square_fits(skyline, available)) {
|
||||||
|
if constexpr (Instrument) {
|
||||||
|
++counters->prune_hits;
|
||||||
|
++counters->large_square_prunes;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
auto const largest =
|
auto const largest =
|
||||||
std::min({n, valley.width, length - valley.height});
|
std::min({n, valley.width, length - valley.height});
|
||||||
auto try_side = [&](size_t const side) {
|
auto try_side = [&](size_t const side) {
|
||||||
@@ -320,7 +376,8 @@ namespace {
|
|||||||
side, valley.height + side);
|
side, valley.height + side);
|
||||||
squares.emplace_back(valley.x + valley.height * length, side);
|
squares.emplace_back(valley.x + valley.height * length, side);
|
||||||
|
|
||||||
if (search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity>(
|
if (search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity,
|
||||||
|
PruneLargeSquare>(
|
||||||
n, length, policy, skyline, available, squares, counters)) {
|
n, length, policy, skyline, available, squares, counters)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -367,7 +424,8 @@ namespace {
|
|||||||
* candidates and updating up to n columns for each costs O(n^2), for
|
* candidates and updating up to n columns for each costs O(n^2), for
|
||||||
* O(board width + n^2) local work per node and the same total state.
|
* O(board width + n^2) local work per node and the same total state.
|
||||||
*/
|
*/
|
||||||
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity>
|
template<bool Instrument, bool BreakD4Symmetry, bool PruneValleyCapacity,
|
||||||
|
bool PruneLargeSquare>
|
||||||
auto search_solution_impl(size_t const n, SearchPolicy const policy,
|
auto search_solution_impl(size_t const n, SearchPolicy const policy,
|
||||||
SearchCounters *const counters) noexcept
|
SearchCounters *const counters) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
@@ -380,29 +438,50 @@ namespace {
|
|||||||
std::vector<Square> squares;
|
std::vector<Square> squares;
|
||||||
squares.reserve(length);
|
squares.reserve(length);
|
||||||
static_cast<void>(
|
static_cast<void>(
|
||||||
search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity>(
|
search_skyline<Instrument, BreakD4Symmetry, PruneValleyCapacity,
|
||||||
|
PruneLargeSquare>(
|
||||||
n, length, policy, skyline, available, squares, counters));
|
n, length, policy, skyline, available, squares, counters));
|
||||||
|
|
||||||
return {length, std::move(squares)};
|
return {length, std::move(squares)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<bool Instrument, bool BreakD4Symmetry>
|
||||||
|
auto search_solution_dispatch(size_t const n, SearchPolicy const policy,
|
||||||
|
Pruning const pruning,
|
||||||
|
SearchCounters *const counters) noexcept
|
||||||
|
-> 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);
|
||||||
|
}
|
||||||
|
assert(false);
|
||||||
|
return search_solution_impl<Instrument, BreakD4Symmetry, false, false>(
|
||||||
|
n, policy, counters);
|
||||||
|
}
|
||||||
|
|
||||||
auto search_solution(
|
auto search_solution(
|
||||||
size_t const n,
|
size_t const n,
|
||||||
SearchPolicy const policy = SearchPolicy::ascending,
|
SearchPolicy const policy = SearchPolicy::ascending,
|
||||||
SymmetryBreaking const symmetry =
|
SymmetryBreaking const symmetry =
|
||||||
SymmetryBreaking::d4_unit_square,
|
SymmetryBreaking::d4_unit_square,
|
||||||
Pruning const pruning = Pruning::valley_capacity) noexcept
|
Pruning const pruning = Pruning::all) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
||||||
if (pruning == Pruning::valley_capacity) {
|
return search_solution_dispatch<false, true>(
|
||||||
return search_solution_impl<false, true, true>(n, policy, nullptr);
|
n, policy, pruning, nullptr);
|
||||||
}
|
}
|
||||||
return search_solution_impl<false, true, false>(n, policy, nullptr);
|
return search_solution_dispatch<false, false>(
|
||||||
}
|
n, policy, pruning, nullptr);
|
||||||
if (pruning == Pruning::valley_capacity) {
|
|
||||||
return search_solution_impl<false, false, true>(n, policy, nullptr);
|
|
||||||
}
|
|
||||||
return search_solution_impl<false, false, false>(n, policy, nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto search_solution_instrumented(size_t const n,
|
auto search_solution_instrumented(size_t const n,
|
||||||
@@ -412,21 +491,15 @@ namespace {
|
|||||||
SymmetryBreaking const symmetry =
|
SymmetryBreaking const symmetry =
|
||||||
SymmetryBreaking::d4_unit_square,
|
SymmetryBreaking::d4_unit_square,
|
||||||
Pruning const pruning =
|
Pruning const pruning =
|
||||||
Pruning::valley_capacity) noexcept
|
Pruning::all) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
counters = {};
|
counters = {};
|
||||||
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
||||||
if (pruning == Pruning::valley_capacity) {
|
return search_solution_dispatch<true, true>(
|
||||||
return search_solution_impl<true, true, true>(
|
n, policy, pruning, &counters);
|
||||||
n, policy, &counters);
|
|
||||||
}
|
}
|
||||||
return search_solution_impl<true, true, false>(n, policy, &counters);
|
return search_solution_dispatch<true, false>(
|
||||||
}
|
n, policy, pruning, &counters);
|
||||||
if (pruning == Pruning::valley_capacity) {
|
|
||||||
return search_solution_impl<true, false, true>(
|
|
||||||
n, policy, &counters);
|
|
||||||
}
|
|
||||||
return search_solution_impl<true, false, false>(n, policy, &counters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Construct an odd-order solution from its even-order predecessor. */
|
/** Construct an odd-order solution from its even-order predecessor. */
|
||||||
@@ -464,7 +537,7 @@ namespace {
|
|||||||
SearchPolicy const policy = SearchPolicy::ascending,
|
SearchPolicy const policy = SearchPolicy::ascending,
|
||||||
SymmetryBreaking const symmetry =
|
SymmetryBreaking const symmetry =
|
||||||
SymmetryBreaking::d4_unit_square,
|
SymmetryBreaking::d4_unit_square,
|
||||||
Pruning const pruning = Pruning::valley_capacity) noexcept -> Results {
|
Pruning const pruning = Pruning::all) noexcept -> Results {
|
||||||
if (uses_odd_construction(n)) {
|
if (uses_odd_construction(n)) {
|
||||||
return construct_odd_solution(
|
return construct_odd_solution(
|
||||||
n, search_solution(n - 1, policy, symmetry, pruning));
|
n, search_solution(n - 1, policy, symmetry, pruning));
|
||||||
@@ -479,7 +552,7 @@ namespace {
|
|||||||
SymmetryBreaking const symmetry =
|
SymmetryBreaking const symmetry =
|
||||||
SymmetryBreaking::d4_unit_square,
|
SymmetryBreaking::d4_unit_square,
|
||||||
Pruning const pruning =
|
Pruning const pruning =
|
||||||
Pruning::valley_capacity) noexcept
|
Pruning::all) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
if (uses_odd_construction(n)) {
|
if (uses_odd_construction(n)) {
|
||||||
return construct_odd_solution(
|
return construct_odd_solution(
|
||||||
|
|||||||
@@ -369,6 +369,8 @@ namespace {
|
|||||||
failures += expect(counters.prune_checks >= counters.prune_hits &&
|
failures += expect(counters.prune_checks >= counters.prune_hits &&
|
||||||
counters.valley_capacity_checks >=
|
counters.valley_capacity_checks >=
|
||||||
counters.valley_capacity_prunes &&
|
counters.valley_capacity_prunes &&
|
||||||
|
counters.large_square_checks >=
|
||||||
|
counters.large_square_prunes &&
|
||||||
counters.generated_tasks == 0 &&
|
counters.generated_tasks == 0 &&
|
||||||
counters.completed_tasks == 0,
|
counters.completed_tasks == 0,
|
||||||
"search counters are inconsistent");
|
"search counters are inconsistent");
|
||||||
@@ -582,6 +584,71 @@ namespace {
|
|||||||
return failures;
|
return failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto test_large_square_pruning() -> int {
|
||||||
|
int failures = 0;
|
||||||
|
auto const fragmented =
|
||||||
|
std::vector<std::uint64_t>{0, 5, 0, 5, 0, 5};
|
||||||
|
failures += expect(
|
||||||
|
!skyline_has_empty_square(fragmented, 2),
|
||||||
|
"large-square check accepted a fragmented state without a 2x2 box");
|
||||||
|
failures += expect(
|
||||||
|
skyline_has_empty_square(
|
||||||
|
std::vector<std::uint64_t>{0, 0, 5, 5, 5, 5}, 2),
|
||||||
|
"large-square check rejected an available 2x2 box");
|
||||||
|
|
||||||
|
Avail available(4);
|
||||||
|
available[1] = 1;
|
||||||
|
available[2] = 1;
|
||||||
|
failures += expect(
|
||||||
|
!remaining_large_square_fits(fragmented, available),
|
||||||
|
"remaining-square check ignored the impossible largest square");
|
||||||
|
available[2] = 0;
|
||||||
|
failures += expect(
|
||||||
|
remaining_large_square_fits(fragmented, available),
|
||||||
|
"remaining-square check did not use geometric size monotonicity");
|
||||||
|
|
||||||
|
// The fragmented profile has 21 empty cells, more than the area of the
|
||||||
|
// remaining 2x2 square, but no two adjacent columns have two free rows.
|
||||||
|
std::uint64_t filled_area = 0;
|
||||||
|
for (auto const height: fragmented) {
|
||||||
|
filled_area += height;
|
||||||
|
}
|
||||||
|
failures += expect(
|
||||||
|
filled_area <= 36 - 4,
|
||||||
|
"fragmented test state does not have sufficient total empty area");
|
||||||
|
|
||||||
|
for (auto const order: std::array<std::uint64_t, 5>{1, 2, 3, 4, 5}) {
|
||||||
|
SearchCounters pruned_counters;
|
||||||
|
SearchCounters baseline_counters;
|
||||||
|
auto const pruned = search_solution_instrumented(
|
||||||
|
order, pruned_counters, SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking::disabled, Pruning::all);
|
||||||
|
auto const baseline = search_solution_instrumented(
|
||||||
|
order, baseline_counters, SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking::disabled, Pruning::valley_capacity);
|
||||||
|
failures += expect(
|
||||||
|
pruned.squares().empty() == baseline.squares().empty(),
|
||||||
|
"large-square pruning changed exhaustive order-" +
|
||||||
|
std::to_string(order) + " feasibility");
|
||||||
|
failures += expect(
|
||||||
|
pruned_counters.search_nodes <= baseline_counters.search_nodes,
|
||||||
|
"large-square 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::large_square));
|
||||||
|
failures += expect(
|
||||||
|
counters.large_square_checks > 0 &&
|
||||||
|
counters.large_square_prunes > 0 &&
|
||||||
|
counters.prune_checks == counters.large_square_checks &&
|
||||||
|
counters.prune_hits == counters.large_square_prunes,
|
||||||
|
"large-square instrumentation did not count checks and prunes");
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
auto test_solver_completion() -> int {
|
auto test_solver_completion() -> int {
|
||||||
int failures = 0;
|
int failures = 0;
|
||||||
for (auto const order: std::array<std::uint64_t, 2>{1, 8}) {
|
for (auto const order: std::array<std::uint64_t, 2>{1, 8}) {
|
||||||
@@ -633,6 +700,9 @@ int main(int argc, char **argv) {
|
|||||||
if (test == "valley-capacity") {
|
if (test == "valley-capacity") {
|
||||||
return test_valley_capacity_pruning();
|
return test_valley_capacity_pruning();
|
||||||
}
|
}
|
||||||
|
if (test == "large-square") {
|
||||||
|
return test_large_square_pruning();
|
||||||
|
}
|
||||||
std::cerr << "unknown test: " << test << '\n';
|
std::cerr << "unknown test: " << test << '\n';
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user