solver: add optional component-area pruning

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
This commit was merged in pull request #28.
This commit is contained in:
Codex instance
2026-07-31 08:40:39 +01:00
parent 220cec06a9
commit 39ff5cb340
6 changed files with 491 additions and 53 deletions
+60
View File
@@ -24,6 +24,10 @@ otherwise identical unconstrained baseline.
The production default also applies the pruning rules described below.
Use `--pruning valley-capacity`, `--pruning large-square`, or
`--no-pruning` to measure each rule alone or obtain an unpruned search.
Use `--component-pruning gcd`, `--component-pruning subset-sum`, or
`--component-pruning none` to compare the component-area rule separately.
Component pruning is disabled by default because the measurements below do not
recover its cost.
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:
@@ -157,6 +161,62 @@ is already recovered; it was not added without evidence that the complexity
would improve elapsed time. The simple every-node scan remains enabled by
default for the measured public benchmark benefit.
## Component-area pruning
In a skyline, every non-full column is empty from its filled height to the top
of the board. Adjacent non-full columns therefore belong to the same empty
component, while a full-height column is an impassable separator. Each
remaining square must lie wholly within one such component, so every component
area must be the sum of a bounded subset of the remaining square areas.
The `gcd` mode first rejects a component area which is not divisible by the
greatest common divisor of all remaining square areas. The `subset-sum` mode
then computes exact reachable areas using each remaining multiplicity as a
bound. Each component is checked against the same reachable set; this is a
necessary condition, not a claim that independently selected subsets are
mutually disjoint.
The check is triggered only after a placement reaches full board height and
can create or extend a component boundary. This keeps the potentially more
expensive bounded subset sum off ordinary nodes. Instrumented output records
`component_area_checks`, `component_area_prunes`, `component_gcd_prunes`, and
`component_subset_prunes`. The three `--component-pruning` modes allow the
trigger cost, cheap gcd rule, and bounded subset sum to be compared directly.
Use `--component-schedule periodic-8` to compare the event-driven boundary
trigger with checking every eighth placement depth.
Measurements used the issue #13 working tree based on commit `220cec0`, Apple
Clang 21.0.0, `-O3 -DNDEBUG`, macOS arm64, one worker, one warm-up, and seven
measured repetitions for the boundary-trigger modes. Counter-free and counted
runs were interleaved; the table reports counter-free medians. The existing
valley-capacity and large-square rules remained enabled. All results passed
independent validation and counters were stable:
| Order | Component rule | Median solve | Nodes | Checks | Prunes |
| --- | --- | ---: | ---: | ---: | ---: |
| 7 exhaustive | disabled | 0.985 s | 13,221,239 | 0 | 0 |
| 7 exhaustive | gcd | 1.027 s | 13,220,729 | 171,088 | 2,568 |
| 7 exhaustive | subset sum | 1.052 s | 13,189,961 | 161,706 | 69,483 |
| 8 first solution | disabled | 0.203 s | 2,597,678 | 0 | 0 |
| 8 first solution | gcd | 0.212 s | 2,597,548 | 23,943 | 637 |
| 8 first solution | subset sum | 0.219 s | 2,592,212 | 23,097 | 11,858 |
Gcd-only checking changed fewer than 0.005% of nodes while slowing the
counter-free solver by 4.3% for both orders. Bounded subset sum reduced nodes
by only 0.24% for order 7 and 0.21% for order 8, while slowing them by 6.8% and
7.5%. Neither rule is enabled by default.
The boundary trigger is an incremental event check: it runs only when the
latest placement reaches full height and can change the component partition.
For comparison, subset sum was also sampled every eighth placement depth with
one warm-up and three measured repetitions. Periodic checking made 1,501,035
checks for order 7 and 290,774 for order 8, versus 161,706 and 23,097 at
boundary events. Its counter-free medians were 1.088 s and 0.227 s, 10.5% and
11.7% slower than disabled pruning and materially worse than boundary
triggering. The periodic schedule is retained only as an opt-in measurement
mode; event-triggered checking is the cheaper schedule if component pruning is
reconsidered with new evidence.
## D4 board symmetry
Every solution contains exactly one 1-by-1 square. Rotations and reflections