Full-height skyline columns partition the remaining board. Add sound gcd and bounded subset-sum checks for the resulting component areas, with boundary-event and periodic benchmark schedules.
Keep the rules disabled by default because their small tree reductions do not recover their measured cost. Record the rejected default and scheduling evidence so it can be revisited only with new data.
Tests: Debug CTest (14 passed)
Tests: ASan+UBSan CTest (14 passed)
Refs: #13
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
A selected valley cannot admit a square wider than itself until it reaches the lower neighbouring rim. Reject states whose remaining narrow-square area cannot fill that strip, including width-one and width-two gaps.
Keep an unpruned benchmark mode and dedicated counters so the rule remains independently measurable. Record the soundness argument and the measured default-on improvement.
Tests: Debug CTest (12 passed)
Tests: ASan+UBSan CTest (12 passed)
Refs: #10
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
Make candidate ordering an explicit deterministic policy and benchmark ascending, descending, and exact-width-first choices. Keep ascending as the default because it produces the best measured time to first solution despite best-fit's slightly smaller tree.
Retain the benchmark v1 interface and document why randomized and duplicate-work portfolio policies are deferred.
Tests: Release and Debug CTest (10 passed each)
Refs: #12
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
Provide a separately installed OR-Tools model to compare a generic constraint solver with the native first-solution path without adding a production or default-test dependency.
Use no-overlap, exact-fill, edge and equal-copy symmetry constraints, validate placements independently, and record model size, memory, worker count and timings for orders 8 and 9. Keep the tool only as a reference and defer DLX absent new evidence.
Tests: Release CTest (9 passed)
Tests: Python reference tests and compilation checks
Tests: independently validated CP-SAT orders 8 and 9
Refs: #5
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
Record reproducible order-7 and order-8 measurements from the committed benchmark framework with a clean working tree. Include stable work counts, solve-time spread, validation and rendering cost, and interleaved instrumentation overhead.
Order 9 remains supported by the runner but retains its historical timing because each current run takes several minutes.
Benchmark: Apple arm64 Release, one warm-up, three repetitions
Refs: #8
Add an opt-in benchmark probe and JSON runner that separate solve, construction, validation, and rendering time. Record stable search counters, environment metadata, warm-up and repetition policy, timeouts, errors, median spread, and instrumentation overhead.
Compile production solving without counters and interleave counted and plain trials when measuring overhead. Keep heavyweight cases outside the default correctness path while testing counter and report behavior cheaply.
Tests: Debug and Release CTest suites (7 passed each)
Refs: #8
next_pos() returns the grid end sentinel when a placement completes the board. Avoid passing that sentinel to largest_square(), which requires an in-range position and otherwise reads past the grid.
Tests: Debug, Release, ASan, and UBSan CTest suites
Refs: #14
Exercise both the trivial order-1 solution and the feasible order-8 solution through the independent placement validator.
With the existing completion ordering, the focused test reports a heap-buffer-overflow under AddressSanitizer before validation can run.
Refs: #14
Check the character in the output string instead of referring to a nonexistent grid_ member. This restores Debug compilation while preserving the intended invariant that rendering must not write an identical character twice.
Size labels intentionally replace interior spaces, so requiring an entirely unwritten destination would reject valid solutions. Update the testing notes now that Debug builds pass.
Tests: Debug and Release CTest suites (4 passed each)
Refs: #7
Exercise Results::output() with the known order-8 fixture so Debug builds compile and run the rendering assertions. Check the output dimensions and ensure the valid tiling leaves no unrendered cells.
The test passes in Release and currently fails to compile in Debug because Results::set() refers to the nonexistent grid_ member.
Refs: #7
Establish a CTest harness before changing solver behavior. Validate board dimensions, multiplicities, bounds, overlap and coverage against independent placement data, including known order-8 and constructed order-9 fixtures.
Document Debug and sanitizer workflows while keeping the defects tracked by #7 and #14 separate. Prepare a result adapter so the feasible solver regression can be enabled with the completion fix.
Tests: Release, ASan and UBSan CTest suites (3 passed each)
Refs: #1
The goal of this sequence of patches is to end up representing a
position as a single integer index into the grid instead of an (x, y)
pair. I believe this will have better performance.
First step move all calls of x(), y() into calls on methods of Results &
Grid as in the future these will need to use the length of the grid.
We abstract away the types being used from 'int' and 'char'. The one
exception being for output.
This allows us to experiment to see if using different types can improve
performance.
It turns out it does - and that using 64-bit integers everywhere is a
good idea.
We now return the results in a separate structure to the grid we have
been working on.
This ultimately makes it easier to play with the implementation of the
solution finder.
Previously our implementation was recursive (although less so than the
OCaml version).
Moving to an iterative implementation improves performance as we have
fewer function calls, and so fewer function prologues and epilogues.
The initial implementation of the code stored squares in the grid in a
"pretty" form. This is a complicated bit of code and slowed down the
code slightly.
Now we just store the size of the current square in the grid. This
reduces the code complexity, and offers a slight performance
improvement.
We still output a prettified version of the grid, as we can generate
that from a grid populated with just square sizes.
This is another optimisation, as it improves memory layout and we are
not using a reference counted string.
On a M1 Macbook Air this brings runtime for solving the 9 cell down to
under 3m30s. Which is about a 15% performance improvement.
We were not taking into account that the largest_square may be
constrained by the y position as well as the x. i.e. is there enough
vertical space for the square.
This commit adds that check.
We were working before because C++ is not memory safe, and we weren't
running debug builds.
Grid::next_pos was starting its scan from the position after the current
one.
This is inefficient as we know how large the square we've just placed is
as we scan those positions.
This optimisation just skips that square when starting the scan.
As an optimisation we know that if a square with side length N fits in a
certain position, then one of side length N - 1 does too. And
conversely, if a square of side length N does not fit then one of side
length N + 1 does not.
So our implementation which asked if every square fitted in a certain
position called Grid::fits() too many times, once for each side length.
We replace this by a function Grid::largest_square which gives the
appropriate side length to start with.
Naïvely the optimisation here is that instead of checking whether N
squares fit, and so having to do (N * (N + 1)) / 2 comparisons in fits
per position, we instead do one scan in largest_square and do at most N
comparisons.
A performance improvement is also seen in real life.
We stop using function calls to move down an index but instead use a for
loop.
This actually provides a slight performance improvement as we are not
repeated making and destroying function frames.