solver: break board dihedral symmetry

Constrain the unique unit square to a closed D4 fundamental region once its placement is known. This preserves one representative of every board-orientation orbit without assigning identities to repeated squares.

Keep a symmetry-disabled benchmark path, document the proof and measurements, and cover generic, diagonal, midline, corner, and centre orbits.

Tests: Release, Debug, ASan, and UBSan CTest (11 passed each)

Refs: #3
This commit was merged in pull request #25.
This commit is contained in:
Codex instance
2026-07-30 18:18:45 +01:00
parent 74bde266d0
commit f37e08768d
7 changed files with 284 additions and 28 deletions
+68 -15
View File
@@ -162,6 +162,29 @@ namespace {
best_fit,
};
/** Whether to remove equivalent board orientations from the search. */
enum class SymmetryBreaking {
disabled,
d4_unit_square,
};
/** 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
* from the left edge is no greater than its distance from the top edge, and
* finally reflect it into the top half. The resulting fundamental region is
* the closed triangle x <= y <= floor((length - 1) / 2). Closed boundaries
* retain representatives whose orbit is smaller than eight.
*/
[[nodiscard]] auto canonical_unit_position(size_t const x,
size_t const y,
size_t const length) noexcept
-> bool {
assert(x < length);
assert(y < length);
return x <= y && y <= (length - 1) / 2;
}
/** A maximal level skyline segment which is lower than its neighbours. */
struct Valley {
size_t x;
@@ -200,7 +223,7 @@ namespace {
return best;
}
template<bool Instrument>
template<bool Instrument, bool BreakD4Symmetry>
auto search_skyline(size_t const n, size_t const length,
SearchPolicy const policy,
std::vector<size_t> &skyline, Avail &available,
@@ -226,6 +249,20 @@ namespace {
return false;
}
if constexpr (BreakD4Symmetry) {
if (side == 1) {
if constexpr (Instrument) {
++counters->prune_checks;
}
if (!canonical_unit_position(valley.x, valley.height, length)) {
if constexpr (Instrument) {
++counters->prune_hits;
}
return false;
}
}
}
if constexpr (Instrument) {
++counters->attempted_placements;
}
@@ -234,8 +271,8 @@ namespace {
side, valley.height + side);
squares.emplace_back(valley.x + valley.height * length, side);
if (search_skyline<Instrument>(n, length, policy, skyline,
available, squares, counters)) {
if (search_skyline<Instrument, BreakD4Symmetry>(
n, length, policy, skyline, available, squares, counters)) {
return true;
}
@@ -281,7 +318,7 @@ namespace {
* 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.
*/
template<bool Instrument>
template<bool Instrument, bool BreakD4Symmetry>
auto search_solution_impl(size_t const n, SearchPolicy const policy,
SearchCounters *const counters) noexcept
-> Results {
@@ -293,7 +330,7 @@ namespace {
}
std::vector<Square> squares;
squares.reserve(length);
static_cast<void>(search_skyline<Instrument>(
static_cast<void>(search_skyline<Instrument, BreakD4Symmetry>(
n, length, policy, skyline, available, squares, counters));
return {length, std::move(squares)};
@@ -301,18 +338,28 @@ namespace {
auto search_solution(
size_t const n,
SearchPolicy const policy = SearchPolicy::ascending) noexcept
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept
-> Results {
return search_solution_impl<false>(n, policy, nullptr);
if (symmetry == SymmetryBreaking::d4_unit_square) {
return search_solution_impl<false, true>(n, policy, nullptr);
}
return search_solution_impl<false, false>(n, policy, nullptr);
}
auto search_solution_instrumented(size_t const n,
SearchCounters &counters,
SearchPolicy const policy =
SearchPolicy::ascending) noexcept
SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept
-> Results {
counters = {};
return search_solution_impl<true>(n, policy, &counters);
if (symmetry == SymmetryBreaking::d4_unit_square) {
return search_solution_impl<true, true>(n, policy, &counters);
}
return search_solution_impl<true, false>(n, policy, &counters);
}
/** Construct an odd-order solution from its even-order predecessor. */
@@ -347,23 +394,29 @@ namespace {
auto find_solution(
size_t const n,
SearchPolicy const policy = SearchPolicy::ascending) noexcept -> Results {
SearchPolicy const policy = SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept -> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(n, search_solution(n - 1, policy));
return construct_odd_solution(
n, search_solution(n - 1, policy, symmetry));
}
return search_solution(n, policy);
return search_solution(n, policy, symmetry);
}
auto find_solution_instrumented(size_t const n,
SearchCounters &counters,
SearchPolicy const policy =
SearchPolicy::ascending) noexcept
SearchPolicy::ascending,
SymmetryBreaking const symmetry =
SymmetryBreaking::d4_unit_square) noexcept
-> Results {
if (uses_odd_construction(n)) {
return construct_odd_solution(
n, search_solution_instrumented(n - 1, counters, policy));
n, search_solution_instrumented(
n - 1, counters, policy, symmetry));
}
return search_solution_instrumented(n, counters, policy);
return search_solution_instrumented(n, counters, policy, symmetry);
}
} // anon namespace