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
+36 -9
View File
@@ -26,26 +26,29 @@ namespace {
double construction_seconds;
};
auto solve(std::uint64_t order, bool instrument, SearchCounters &counters)
auto solve(std::uint64_t order, bool instrument,
CandidateOrder candidate_order, bool direct_search,
SearchCounters &counters)
-> TimedSolution {
auto const predecessor_order =
uses_odd_construction(order) ? order - 1 : order;
!direct_search && 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);
? search_solution_instrumented(predecessor_order, counters,
candidate_order)
: search_solution(predecessor_order, candidate_order);
auto const search_end = Clock::now();
auto const construction_begin = Clock::now();
auto result = uses_odd_construction(order)
auto result = !direct_search && 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)
!direct_search && uses_odd_construction(order)
? seconds(construction_begin, construction_end)
: 0.0,
};
@@ -90,8 +93,9 @@ namespace {
}
int main(int argc, char **argv) {
if (argc != 3) {
std::cerr << "usage: partridge_benchmark ORDER counters|plain\n";
if (argc < 3 || argc > 5) {
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
"[ascending|descending] [public|direct]\n";
return 2;
}
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
@@ -100,9 +104,26 @@ int main(int argc, char **argv) {
std::cerr << "counter mode must be counters or plain\n";
return 2;
}
auto const candidate_order =
argc < 4 || std::string_view(argv[3]) == "ascending"
? CandidateOrder::ascending
: CandidateOrder::descending;
if (argc >= 4 && std::string_view(argv[3]) != "ascending" &&
std::string_view(argv[3]) != "descending") {
std::cerr << "candidate order must be ascending or descending\n";
return 2;
}
auto const direct_search =
argc == 5 && std::string_view(argv[4]) == "direct";
if (argc == 5 && std::string_view(argv[4]) != "public" &&
std::string_view(argv[4]) != "direct") {
std::cerr << "search route must be public or direct\n";
return 2;
}
SearchCounters counters;
auto timed = solve(order, instrument, counters);
auto timed =
solve(order, instrument, candidate_order, direct_search, counters);
auto const validation_begin = Clock::now();
auto const validation_ok = valid(order, timed.result);
@@ -120,6 +141,12 @@ int main(int argc, char **argv) {
<< "{\"schema_version\":1"
<< ",\"order\":" << order
<< ",\"instrumented\":" << boolean(instrument)
<< ",\"candidate_order\":\""
<< (candidate_order == CandidateOrder::ascending ? "ascending"
: "descending")
<< "\""
<< ",\"search_route\":\""
<< (direct_search ? "direct" : "public") << "\""
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
<< ",\"valid\":" << boolean(validation_ok)
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
+27 -5
View File
@@ -74,16 +74,16 @@ def environment(binary):
"logical_cpus": os.cpu_count(),
},
"workers": 1,
"search_policy": "single-threaded, deterministic, largest-fitting-first",
"search_policy": "single-threaded, deterministic, smallest-width valley",
"seed": None,
}
def run_once(binary, order, mode, timeout):
def run_once(binary, order, mode, candidate_order, search_route, timeout):
started = time.monotonic()
try:
process = subprocess.run(
[str(binary), str(order), mode],
[str(binary), str(order), mode, candidate_order, search_route],
check=False,
capture_output=True,
text=True,
@@ -216,6 +216,12 @@ def main():
parser.add_argument("--warmup", type=int, default=1)
parser.add_argument("--repetitions", type=int, default=5)
parser.add_argument("--timeout", type=float, default=600.0)
parser.add_argument(
"--candidate-order",
choices=("ascending", "descending"),
default="ascending",
)
parser.add_argument("--direct-search", action="store_true")
parser.add_argument(
"--measure-overhead",
action="store_true",
@@ -241,11 +247,25 @@ def main():
mode_results = {mode: {"runs": []} for mode in modes}
for trial in range(args.warmup):
for mode in trial_modes(modes, trial):
run_once(args.binary, order, mode, args.timeout)
run_once(
args.binary,
order,
mode,
args.candidate_order,
"direct" if args.direct_search else "public",
args.timeout,
)
for trial in range(args.repetitions):
for mode in trial_modes(modes, trial):
mode_results[mode]["runs"].append(
run_once(args.binary, order, mode, args.timeout)
run_once(
args.binary,
order,
mode,
args.candidate_order,
"direct" if args.direct_search else "public",
args.timeout,
)
)
for result in mode_results.values():
result["summary"] = summarize(result["runs"])
@@ -270,6 +290,8 @@ def main():
"warmup_runs": args.warmup,
"measured_repetitions": args.repetitions,
"per_run_timeout_seconds": args.timeout,
"candidate_order": args.candidate_order,
"search_route": "direct" if args.direct_search else "public",
"stdout": "captured; rendered grid suppressed by probe",
},
"cases": cases,