solver: replace cell DFS with skyline search

Represent partial placements as column heights and branch on the narrowest local valley. This removes the board-area cell state and makes first-solution search substantially smaller for feasible orders.

Expose direct-search and candidate-order benchmark controls so the skyline core can be measured independently of odd-order construction. Document the completeness argument and the 10/11 test-tier decision.

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

Refs: #4
This commit was merged in pull request #23.
This commit is contained in:
Codex instance
2026-07-30 17:55:21 +01:00
parent 0a7ce1e49e
commit d751d1b13e
7 changed files with 296 additions and 198 deletions
+59 -4
View File
@@ -15,16 +15,25 @@ python3 benchmarks/run.py --binary build-benchmark/partridge_benchmark \
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;
the probe renders into an in-memory stream so grids do not perturb terminal I/O.
Override the policy with `--orders`, `--warmup`, `--repetitions`, and
`--timeout`. 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:
Override the policy with `--orders`, `--warmup`, `--repetitions`, `--timeout`,
and `--candidate-order`. Ascending candidate sizes are the production default.
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:
```sh
python3 benchmarks/run.py --binary build-benchmark/partridge_benchmark \
--orders 8 9 --warmup 1 --repetitions 5 --timeout 60 > benchmark.json
```
Use `--direct-search` when benchmarking the skyline core rather than the public
even-predecessor construction used for odd orders from 9 onwards:
```sh
python3 benchmarks/run.py --binary build-benchmark/partridge_benchmark \
--orders 6 7 8 9 --warmup 1 --repetitions 5 --timeout 60 \
--candidate-order ascending --direct-search > direct.json
```
The JSON contains every run and median, range, and median absolute deviation
for solve, construction, independent validation, and rendering. Direct-search
cases report zero construction time: their setup and allocation remain part of
@@ -59,6 +68,52 @@ Do not use wall-clock thresholds as correctness checks. Keep the generated
JSON outside version control unless it is being deliberately added as a named
comparison baseline.
## Smallest-valley skyline
The solver stores one filled height per board column instead of one value per
cell. Equal adjacent heights form conceptual vertical bars. A valley is a
maximal bar lower than both neighbours, with board edges treated as bars of
full board height. Each node scans for the smallest-width valley, breaking
ties by lower height and then leftmost position, and tries every available
square which fits at that valley's far-left edge.
This branching remains complete: the bottom-left cell of the selected valley
must be covered, a square covering it cannot begin to the left across the
taller neighbour, and it cannot extend beyond the equal-height run without
overlap or leaving an unreachable hole. Trying every fitting available size
therefore includes the placement used by every possible completion.
For board width `W` and order `n`, the skyline scan is `O(W)`. A node tries at
most `n` candidates and each placement or exact undo changes at most `n`
heights, giving `O(W + n^2)` local work. The skyline, multiplicities,
placements, and recursion stack use `O(W + n^2)` state, compared with the
former `O(W^2)` cell grid.
One-run exploratory measurements used the issue #4 dirty worktree at base
commit `0a7ce1e`, Apple Clang 21.0.0, `-O3 -DNDEBUG`, macOS arm64, one worker,
no warm-up, and a 15-second timeout. Every completed result passed the
independent benchmark validator:
| Order | Result | Ascending time | Ascending nodes | Descending time | Descending nodes |
| --- | --- | ---: | ---: | ---: | ---: |
| 6 | infeasible | 0.040 s | 659,598 | 0.039 s | 659,598 |
| 7 | infeasible | 3.103 s | 43,604,507 | 3.071 s | 43,604,507 |
| 8 | solution | 0.585 s | 7,735,369 | 0.941 s | 12,186,125 |
| 9 direct | solution | 3.831 s | 45,840,266 | timeout | unavailable |
The infeasible orders exhaust the same tree in either direction. Ascending
was selected as the default because it reaches the first order-8 solution with
36% fewer nodes and also completed direct order 9 within the timeout;
descending direct order 9 did not.
A direct ascending order-10 probe exceeded 20 seconds. Public order 10 is
also a direct search, and public order 11 first searches order 10 before using
odd-predecessor construction. Consequently neither 10 nor 11 is in the
routine correctness suite: doing so would test the same unresolved order-10
search bottleneck, while the existing route-boundary test still verifies that
11 selects construction. Revisit both sizes when order 10 completes within a
practical test budget.
## Post-correctness baseline
This framework starts from commit `ce39d0a` after the rendering assertion fix