solver: construct odd-order solutions

Avoid repeating the exponential search for odd orders at least nine. Search the even predecessor, translate its row-major placements to the enlarged board, and tile the new border.

Keep direct search and construction explicit so benchmarks can report their costs separately. Verify the routed order-9 result independently and require its search counters to match order 8.

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

Refs: #6
This commit was merged in pull request #21.
This commit is contained in:
Codex instance
2026-07-30 17:24:06 +01:00
parent ddf07e730a
commit 3e667d6de0
6 changed files with 189 additions and 44 deletions
+32 -9
View File
@@ -16,19 +16,20 @@ The default policy is one unrecorded warm-up followed by five repetitions per
case, with a 600-second timeout for each process. Solver stdout is captured; case, with a 600-second timeout for each process. Solver stdout is captured;
the probe renders into an in-memory stream so grids do not perturb terminal I/O. the probe renders into an in-memory stream so grids do not perturb terminal I/O.
Override the policy with `--orders`, `--warmup`, `--repetitions`, and Override the policy with `--orders`, `--warmup`, `--repetitions`, and
`--timeout`. Order 9 is intentionally supported but may be omitted during `--timeout`. Order 9 uses the constructive odd-order path, searching order 8
local iteration because the current solver takes minutes: and then tiling the enlarged border, so it is suitable for normal local
benchmarking:
```sh ```sh
python3 benchmarks/run.py --binary build-benchmark/partridge_benchmark \ python3 benchmarks/run.py --binary build-benchmark/partridge_benchmark \
--orders 7 8 --warmup 1 --repetitions 5 --timeout 60 > benchmark.json --orders 8 9 --warmup 1 --repetitions 5 --timeout 60 > benchmark.json
``` ```
The JSON contains every run and median, range, and median absolute deviation The JSON contains every run and median, range, and median absolute deviation
for solve, construction, independent validation, and rendering. The current for solve, construction, independent validation, and rendering. Direct-search
direct-search solver reports zero construction time: its setup and allocation cases report zero construction time: their setup and allocation remain part of
remain part of solve time. The separate construction field is reserved for solve time. Constructed odd-order cases report predecessor search and
future constructive solution paths. The document also records compiler, construction separately. The document also records compiler,
flags, build type, commit, OS/CPU metadata, worker count, search policy, seed, flags, build type, commit, OS/CPU metadata, worker count, search policy, seed,
timeouts, errors, invalid outputs, and the stdout policy. Search counts must timeouts, errors, invalid outputs, and the stdout policy. Search counts must
be stable across repeated runs. Prune and task counters are zero for the be stable across repeated runs. Prune and task counters are zero for the
@@ -80,8 +81,30 @@ Counts were stable across repetitions. Interleaved counter-free medians were
overheads of 0.53% and 0.64% respectively. Construction time was zero; median overheads of 0.53% and 0.64% respectively. Construction time was zero; median
independent validation and rendering times were each below 0.02 milliseconds. independent validation and rendering times were each below 0.02 milliseconds.
Order 9 was not rerun for this initial baseline because its documented runtime Order 9 was not rerun for this initial baseline because the former direct
is several minutes. The default suite includes it with a per-run timeout. search took several minutes. The default suite includes it with a per-run
timeout.
## Odd construction comparison
The order-9 construction was measured from the issue 6 working tree based on
commit `ddf07e7`, using Apple Clang 21.0.0 with `-O3 -DNDEBUG`, macOS arm64,
one worker, one warm-up, and three measured repetitions. Counter-free and
counted runs were interleaved:
| Order | Mode | Search median (range) | Construction median | Nodes |
| --- | --- | --- | --- | ---: |
| 8 | counter-free | 1.773 s (1.7701.775 s) | 0 | 0 |
| 8 | counted | 1.849 s (1.8481.850 s) | 0 | 60,485,176 |
| 9 | counter-free | 1.778 s (1.7731.779 s) | 0.458 us | 0 |
| 9 | counted | 1.852 s (1.8451.912 s) | 0.416 us | 60,485,176 |
All runs completed with valid results and stable counters. The matching
order-8 and order-9 search counts demonstrate that the new path searches only
the predecessor. Compared with the recorded 158.69-second direct order-9
elapsed time in `results.md`, the 1.778-second counter-free median plus
construction is approximately 89 times faster. The benchmark working tree
was necessarily dirty with the issue 6 implementation.
New optimization issues should quote the exact JSON New optimization issues should quote the exact JSON
environment, policy, median/spread, stable counters, and counted overhead from environment, policy, median/spread, stable counters, and counted overhead from
+1
View File
@@ -19,6 +19,7 @@ if(BUILD_TESTING)
tests/tests.cc) tests/tests.cc)
add_test(NAME validator COMMAND partridge_tests validator) add_test(NAME validator COMMAND partridge_tests validator)
add_test(NAME construction COMMAND partridge_tests construction) add_test(NAME construction COMMAND partridge_tests construction)
add_test(NAME solver-odd-route COMMAND partridge_tests solver-odd-route)
add_test(NAME rendering COMMAND partridge_tests rendering) add_test(NAME rendering COMMAND partridge_tests rendering)
add_test(NAME solver-small COMMAND partridge_tests solver-small) add_test(NAME solver-small COMMAND partridge_tests solver-small)
add_test(NAME solver-completion COMMAND partridge_tests solver-completion) add_test(NAME solver-completion COMMAND partridge_tests solver-completion)
+4 -2
View File
@@ -12,8 +12,10 @@ ctest --test-dir build --output-on-failure
The tests independently check board dimensions, square multiplicities, bounds, The tests independently check board dimensions, square multiplicities, bounds,
overlap, and complete coverage. They cover small unsatisfiable solver inputs, a overlap, and complete coverage. They cover small unsatisfiable solver inputs, a
known order-8 solution, invalid placement diagnostics, and construction of an known order-8 solution, invalid placement diagnostics, and construction of an
order-9 solution from the order-8 fixture. The rendering test also formats the order-9 solution from the order-8 fixture. The routed order-9 solver test
known order-8 solution and checks the resulting grid dimensions and coverage. checks that its search counters exactly match order 8. The rendering test also
formats the known order-8 solution and checks the resulting grid dimensions and
coverage.
## Debug and sanitizers ## Debug and sanitizers
+37 -9
View File
@@ -20,6 +20,37 @@ namespace {
return std::chrono::duration<double>(end - begin).count(); return std::chrono::duration<double>(end - begin).count();
} }
struct TimedSolution {
Results result;
double search_seconds;
double construction_seconds;
};
auto solve(std::uint64_t order, bool instrument, SearchCounters &counters)
-> TimedSolution {
auto const predecessor_order =
uses_odd_construction(order) ? order - 1 : order;
auto const search_begin = Clock::now();
auto predecessor =
instrument
? search_solution_instrumented(predecessor_order, counters)
: search_solution(predecessor_order);
auto const search_end = Clock::now();
auto const construction_begin = Clock::now();
auto result = uses_odd_construction(order)
? construct_odd_solution(order, std::move(predecessor))
: std::move(predecessor);
auto const construction_end = Clock::now();
return {
std::move(result),
seconds(search_begin, search_end),
uses_odd_construction(order)
? seconds(construction_begin, construction_end)
: 0.0,
};
}
auto valid(std::uint64_t order, Results const &result) -> bool { auto valid(std::uint64_t order, Results const &result) -> bool {
auto const expected = triangle_num(order); auto const expected = triangle_num(order);
if (result.length() != expected) { if (result.length() != expected) {
@@ -71,19 +102,16 @@ int main(int argc, char **argv) {
} }
SearchCounters counters; SearchCounters counters;
auto const solve_begin = Clock::now(); auto timed = solve(order, instrument, counters);
auto result = instrument ? find_solution_instrumented(order, counters)
: find_solution(order);
auto const solve_end = Clock::now();
auto const validation_begin = Clock::now(); auto const validation_begin = Clock::now();
auto const validation_ok = valid(order, result); auto const validation_ok = valid(order, timed.result);
auto const validation_end = Clock::now(); auto const validation_end = Clock::now();
auto const render_begin = Clock::now(); auto const render_begin = Clock::now();
std::ostringstream rendered; std::ostringstream rendered;
auto *const old_buffer = std::cout.rdbuf(rendered.rdbuf()); auto *const old_buffer = std::cout.rdbuf(rendered.rdbuf());
result.output(); timed.result.output();
std::cout.rdbuf(old_buffer); std::cout.rdbuf(old_buffer);
auto const render_end = Clock::now(); auto const render_end = Clock::now();
@@ -92,10 +120,10 @@ int main(int argc, char **argv) {
<< "{\"schema_version\":1" << "{\"schema_version\":1"
<< ",\"order\":" << order << ",\"order\":" << order
<< ",\"instrumented\":" << boolean(instrument) << ",\"instrumented\":" << boolean(instrument)
<< ",\"solved\":" << boolean(!result.squares().empty()) << ",\"solved\":" << boolean(!timed.result.squares().empty())
<< ",\"valid\":" << boolean(validation_ok) << ",\"valid\":" << boolean(validation_ok)
<< ",\"timing_seconds\":{\"solve\":" << seconds(solve_begin, solve_end) << ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
<< ",\"construction\":0" << ",\"construction\":" << timed.construction_seconds
<< ",\"validation\":" << seconds(validation_begin, validation_end) << ",\"validation\":" << seconds(validation_begin, validation_end)
<< ",\"render\":" << seconds(render_begin, render_end) << "}" << ",\"render\":" << seconds(render_begin, render_end) << "}"
<< ",\"counters\":{\"search_nodes\":" << counters.search_nodes << ",\"counters\":{\"search_nodes\":" << counters.search_nodes
+51 -5
View File
@@ -245,12 +245,12 @@ namespace {
size_t completed_tasks = 0; size_t completed_tasks = 0;
}; };
/** Find a solution to the \a n th Partridge problem. /** Search directly for a solution to the \a n th Partridge problem.
* *
* Returns the grid of the solution. * Returns the grid of the solution.
*/ */
template<bool Instrument> template<bool Instrument>
auto find_solution_impl(size_t const n, SearchCounters *const counters) noexcept auto search_solution_impl(size_t const n, SearchCounters *const counters) noexcept
-> Results { -> Results {
/* Implementation is iterative, as opposed to recursive. /* Implementation is iterative, as opposed to recursive.
* *
@@ -343,14 +343,60 @@ namespace {
return {length, sqs}; return {length, sqs};
} }
auto search_solution(size_t const n) noexcept -> Results {
return search_solution_impl<false>(n, nullptr);
}
auto search_solution_instrumented(size_t const n,
SearchCounters &counters) noexcept -> Results {
counters = {};
return search_solution_impl<true>(n, &counters);
}
/** Construct an odd-order solution from its even-order predecessor. */
auto construct_odd_solution(size_t const odd_order, Results predecessor)
-> Results {
assert(odd_order >= 9);
assert(odd_order % 2 == 1);
assert(predecessor.length() == triangle_num(odd_order - 1));
auto const old_length = predecessor.length();
auto const new_length = triangle_num(odd_order);
std::vector<Square> squares;
squares.reserve(predecessor.squares().size() + odd_order);
for (auto const &square: predecessor.squares()) {
auto const x = square.pos() % old_length;
auto const y = square.pos() / old_length;
squares.emplace_back(x + y * new_length, square.length());
}
for (size_t y = 0; y < old_length; y += odd_order) {
squares.emplace_back(old_length + y * new_length, odd_order);
}
for (size_t x = 0; x <= old_length; x += odd_order) {
squares.emplace_back(x + old_length * new_length, odd_order);
}
return {new_length, std::move(squares)};
}
[[nodiscard]] auto uses_odd_construction(size_t const n) noexcept -> bool {
return n >= 9 && n % 2 == 1;
}
auto find_solution(size_t const n) noexcept -> Results { auto find_solution(size_t const n) noexcept -> Results {
return find_solution_impl<false>(n, nullptr); if (uses_odd_construction(n)) {
return construct_odd_solution(n, search_solution(n - 1));
}
return search_solution(n);
} }
auto find_solution_instrumented(size_t const n, auto find_solution_instrumented(size_t const n,
SearchCounters &counters) noexcept -> Results { SearchCounters &counters) noexcept -> Results {
counters = {}; if (uses_odd_construction(n)) {
return find_solution_impl<true>(n, &counters); return construct_odd_solution(
n, search_solution_instrumented(n - 1, counters));
}
return search_solution_instrumented(n, counters);
} }
} // anon namespace } // anon namespace
+63 -18
View File
@@ -8,6 +8,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <span>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -16,6 +17,8 @@ namespace {
std::uint64_t x; std::uint64_t x;
std::uint64_t y; std::uint64_t y;
std::uint64_t side; std::uint64_t side;
auto operator==(Placement const &) const noexcept -> bool = default;
}; };
struct Validation { struct Validation {
@@ -148,21 +151,6 @@ namespace {
return Results(side, std::move(squares)); return Results(side, std::move(squares));
} }
auto construct_next_odd(std::uint64_t even_order,
std::vector<Placement> placements)
-> std::vector<Placement> {
auto const old_side = even_order * (even_order + 1) / 2;
auto const square_side = even_order + 1;
for (std::uint64_t y = 0; y < old_side; y += square_side) {
placements.push_back({old_side, y, square_side});
}
for (std::uint64_t x = 0; x <= old_side; x += square_side) {
placements.push_back({x, old_side, square_side});
}
return placements;
}
auto expect(bool condition, std::string const &message) -> int { auto expect(bool condition, std::string const &message) -> int {
if (condition) { if (condition) {
return 0; return 0;
@@ -225,10 +213,64 @@ namespace {
} }
auto test_construction() -> int { auto test_construction() -> int {
auto const constructed = construct_next_odd(8, known_order_8()); auto predecessor = renderable_result(36, known_order_8());
auto const validation = validate(9, 45, 45, constructed); auto const predecessor_count = predecessor.squares().size();
return expect(validation.valid(), auto const constructed = construct_odd_solution(9, std::move(predecessor));
auto const converted = to_independent(constructed);
auto const validation = validate(9, constructed);
int failures = 0;
failures += expect(
validation.valid(),
"even-to-odd construction was rejected:\n" + validation.text()); "even-to-odd construction was rejected:\n" + validation.text());
failures += expect(constructed.length() == 45,
"constructed board has the wrong side length");
failures += expect(converted.placements.size() == predecessor_count + 9,
"construction did not add exactly nine squares");
auto const expected_border = std::array<Placement, 9>{{
{36, 0, 9}, {36, 9, 9}, {36, 18, 9}, {36, 27, 9},
{0, 36, 9}, {9, 36, 9}, {18, 36, 9}, {27, 36, 9},
{36, 36, 9},
}};
failures += expect(
std::ranges::equal(
std::span(converted.placements).subspan(predecessor_count),
expected_border),
"constructed border coordinates are incorrect");
failures += expect(
std::ranges::equal(
std::span(converted.placements).first(predecessor_count),
known_order_8()),
"construction translated predecessor coordinates unexpectedly");
return failures;
}
auto test_odd_solver_route() -> int {
SearchCounters even_counters;
SearchCounters odd_counters;
auto const even = find_solution_instrumented(8, even_counters);
auto const odd = find_solution_instrumented(9, odd_counters);
int failures = 0;
failures += expect(
!uses_odd_construction(1) && !uses_odd_construction(7) &&
!uses_odd_construction(8) && uses_odd_construction(9) &&
!uses_odd_construction(10) && uses_odd_construction(11),
"odd construction route does not preserve direct handling boundaries");
auto const validation = validate(9, odd);
failures += expect(validation.valid(),
"order-9 routed result is invalid:\n" + validation.text());
failures += expect(odd.squares().size() == even.squares().size() + 9,
"order-9 route did not construct from order 8");
failures += expect(
odd_counters.search_nodes == even_counters.search_nodes &&
odd_counters.loop_iterations == even_counters.loop_iterations &&
odd_counters.attempted_placements ==
even_counters.attempted_placements &&
odd_counters.backtracks == even_counters.backtracks,
"order-9 route did not perform exactly the order-8 search");
return failures;
} }
auto test_rendering() -> int { auto test_rendering() -> int {
@@ -328,6 +370,9 @@ int main(int argc, char **argv) {
if (test == "construction") { if (test == "construction") {
return test_construction(); return test_construction();
} }
if (test == "solver-odd-route") {
return test_odd_solver_route();
}
if (test == "rendering") { if (test == "rendering") {
return test_rendering(); return test_rendering();
} }