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:
+122
-3
@@ -166,6 +166,35 @@ namespace {
|
||||
});
|
||||
}
|
||||
|
||||
auto d4_orbit(Placement const &placement, std::uint64_t const length)
|
||||
-> std::array<Placement, 8> {
|
||||
auto const far_x = length - 1 - placement.x;
|
||||
auto const far_y = length - 1 - placement.y;
|
||||
return {{
|
||||
{placement.x, placement.y, placement.side},
|
||||
{far_y, placement.x, placement.side},
|
||||
{far_x, far_y, placement.side},
|
||||
{placement.y, far_x, placement.side},
|
||||
{far_x, placement.y, placement.side},
|
||||
{far_y, far_x, placement.side},
|
||||
{placement.x, far_y, placement.side},
|
||||
{placement.y, placement.x, placement.side},
|
||||
}};
|
||||
}
|
||||
|
||||
auto canonical_members(Placement const &placement, std::uint64_t const length)
|
||||
-> std::vector<Placement> {
|
||||
auto orbit = d4_orbit(placement, length);
|
||||
std::vector<Placement> members;
|
||||
for (auto const &member: orbit) {
|
||||
if (canonical_unit_position(member.x, member.y, length) &&
|
||||
std::ranges::find(members, member) == members.end()) {
|
||||
members.push_back(member);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
auto test_validator() -> int {
|
||||
int failures = 0;
|
||||
auto const valid = known_order_8();
|
||||
@@ -337,11 +366,98 @@ namespace {
|
||||
failures += expect(counters.attempted_placements > 0 &&
|
||||
counters.backtracks > 0,
|
||||
"instrumented solver did not count placements/backtracks");
|
||||
failures += expect(counters.prune_checks == 0 &&
|
||||
counters.prune_hits == 0 &&
|
||||
failures += expect(counters.prune_checks >= counters.prune_hits &&
|
||||
counters.generated_tasks == 0 &&
|
||||
counters.completed_tasks == 0,
|
||||
"unimplemented solver counters were not zero");
|
||||
"search counters are inconsistent");
|
||||
return failures;
|
||||
}
|
||||
|
||||
auto test_d4_symmetry() -> int {
|
||||
int failures = 0;
|
||||
|
||||
// A generic cell has eight distinct images: identity, three rotations,
|
||||
// and four reflected rotations. Exactly one must survive.
|
||||
auto const generic = d4_orbit({5, 9, 1}, 36);
|
||||
auto distinct_generic = std::vector<Placement>(generic.begin(),
|
||||
generic.end());
|
||||
std::ranges::sort(
|
||||
distinct_generic, {}, [](Placement const &placement) {
|
||||
return std::array{placement.x, placement.y};
|
||||
});
|
||||
distinct_generic.erase(
|
||||
std::ranges::unique(distinct_generic).begin(),
|
||||
distinct_generic.end());
|
||||
failures += expect(distinct_generic.size() == generic.size(),
|
||||
"generic D4 test point does not have eight images");
|
||||
for (std::size_t index = 0; index < generic.size(); ++index) {
|
||||
failures += expect(
|
||||
canonical_members(generic[index], 36) ==
|
||||
std::vector<Placement>{{5, 9, 1}},
|
||||
"D4 transform " + std::to_string(index) +
|
||||
" did not select the same canonical representative");
|
||||
}
|
||||
|
||||
// Closed boundaries are important because diagonal, midline, corner, and
|
||||
// centre cells have non-trivial stabilizers and therefore smaller orbits.
|
||||
for (auto const test_case:
|
||||
std::array<std::pair<std::uint64_t, Placement>, 8>{{
|
||||
{36, {0, 0, 1}},
|
||||
{36, {0, 17, 1}},
|
||||
{36, {7, 7, 1}},
|
||||
{36, {7, 17, 1}},
|
||||
{45, {0, 22, 1}},
|
||||
{45, {11, 11, 1}},
|
||||
{45, {11, 22, 1}},
|
||||
{45, {22, 22, 1}},
|
||||
}}) {
|
||||
failures += expect(
|
||||
canonical_members(test_case.second, test_case.first).size() == 1,
|
||||
"boundary orbit did not retain exactly one distinct representative");
|
||||
}
|
||||
failures += expect(
|
||||
canonical_unit_position(7, 17, 36) &&
|
||||
!canonical_unit_position(8, 7, 36) &&
|
||||
!canonical_unit_position(7, 18, 36) &&
|
||||
canonical_unit_position(22, 22, 45) &&
|
||||
!canonical_unit_position(22, 23, 45),
|
||||
"canonical triangle mishandled a diagonal or midline boundary");
|
||||
|
||||
SearchCounters enabled_counters;
|
||||
SearchCounters disabled_counters;
|
||||
auto const enabled = search_solution_instrumented(
|
||||
8, enabled_counters, SearchPolicy::ascending,
|
||||
SymmetryBreaking::d4_unit_square);
|
||||
auto const disabled = search_solution_instrumented(
|
||||
8, disabled_counters, SearchPolicy::ascending,
|
||||
SymmetryBreaking::disabled);
|
||||
auto enabled_validation = validate(8, enabled);
|
||||
auto disabled_validation = validate(8, disabled);
|
||||
failures += expect(
|
||||
enabled_validation.valid(),
|
||||
"symmetry-enabled solution failed independent validation:\n" +
|
||||
enabled_validation.text());
|
||||
failures += expect(
|
||||
disabled_validation.valid(),
|
||||
"symmetry-disabled solution failed independent validation:\n" +
|
||||
disabled_validation.text());
|
||||
|
||||
auto const unit = std::ranges::find_if(
|
||||
enabled.squares(),
|
||||
[](Square const &square) { return square.length() == 1; });
|
||||
failures += expect(
|
||||
unit != enabled.squares().end() &&
|
||||
canonical_unit_position(unit->pos() % enabled.length(),
|
||||
unit->pos() / enabled.length(),
|
||||
enabled.length()),
|
||||
"symmetry-enabled search placed the unique unit square outside the "
|
||||
"canonical region");
|
||||
failures += expect(
|
||||
enabled_counters.prune_checks > 0 && enabled_counters.prune_hits > 0 &&
|
||||
disabled_counters.prune_checks == 0 &&
|
||||
disabled_counters.prune_hits == 0,
|
||||
"symmetry instrumentation did not distinguish enabled and disabled "
|
||||
"search");
|
||||
return failures;
|
||||
}
|
||||
|
||||
@@ -440,6 +556,9 @@ int main(int argc, char **argv) {
|
||||
if (test == "skyline-search") {
|
||||
return test_skyline_search();
|
||||
}
|
||||
if (test == "d4-symmetry") {
|
||||
return test_d4_symmetry();
|
||||
}
|
||||
std::cerr << "unknown test: " << test << '\n';
|
||||
return 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user