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:
@@ -131,97 +131,6 @@ namespace {
|
||||
std::vector<Square> squares_;
|
||||
};
|
||||
|
||||
/** An N * N grid of characters. */
|
||||
struct Grid {
|
||||
// Type to use for the grid contents
|
||||
using T = std::int_fast64_t;
|
||||
|
||||
/** Construct a grid of given side-length. */
|
||||
explicit Grid(size_t length) : grid_(length * length, empty), length_(length) {
|
||||
}
|
||||
|
||||
Grid(Grid const &other) = delete;
|
||||
|
||||
Grid(Grid &&other) noexcept = default;
|
||||
|
||||
Grid &operator=(Grid const &other) = delete;
|
||||
|
||||
Grid &operator=(Grid &&other) noexcept = default;
|
||||
|
||||
~Grid() noexcept = default;
|
||||
|
||||
/** Get grid length */
|
||||
[[nodiscard]] auto end() const noexcept -> size_t { return static_cast<size_t>(grid_.size()); }
|
||||
|
||||
/** Add a square to the grid. */
|
||||
auto add(Square const &sq) noexcept -> void {
|
||||
/* One would expect the fastest way to do this would be to have x be the
|
||||
* fastest increasing index so we store [pos, pos + 1,..., pos+length, ...]
|
||||
* But experimentation tells us this isn't so, and storing
|
||||
* [pos, pos + length, ..., pos + 1, ...] is faster!
|
||||
*/
|
||||
for (auto x = 0; x < sq.length(); ++x) {
|
||||
for (auto y = sq.pos(); y < sq.pos() + sq.length() * length_; y += length_) {
|
||||
grid_[x + y] = filled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear a square from the grid. */
|
||||
auto clear(Square const &sq) noexcept -> void {
|
||||
for (auto x = 0; x < sq.length(); ++x) {
|
||||
for (auto y = sq.pos(); y < sq.pos() + sq.length() * length_; y += length_) {
|
||||
grid_[x + y] = empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** \brief Get length of the largest square that fits at \a pos in the grid.
|
||||
*/
|
||||
[[nodiscard]] auto largest_square(Pos pos, size_t n) const noexcept -> size_t {
|
||||
assert(pos < end());
|
||||
|
||||
/* Because of how we walk through the grid (starting at 0,0 then increasing
|
||||
* x followed by y) we can assume that if the position (b, y) is clear
|
||||
* (i.e. a '.') then (b, y + i) is clear for all i > 0.
|
||||
*
|
||||
* This means we only need to look for the first non-clear position along the
|
||||
* current row.
|
||||
*/
|
||||
auto const pos_x = pos % length_;
|
||||
auto const pos_y0 = pos - pos_x;
|
||||
auto b = pos;
|
||||
// Make sure we don't go looking in the next row.
|
||||
auto const e = std::min(pos + n, pos_y0 + length_);
|
||||
while (b < e) {
|
||||
if (grid_[b] != empty) { break; }
|
||||
++b;
|
||||
}
|
||||
// Check that this length fits vertically as well.
|
||||
auto const len = b - pos;
|
||||
auto const pos_y = pos / length_;
|
||||
auto const ye = std::min(pos_y + len, length_);
|
||||
return ye - pos_y;
|
||||
}
|
||||
|
||||
/** Get the next position to check starting at pos.
|
||||
*
|
||||
* Returns grid_.length() if no more positions available.
|
||||
*/
|
||||
[[nodiscard]] auto next_pos(Pos pos) const noexcept -> Pos {
|
||||
auto const b = grid_.begin() + static_cast<std::ptrdiff_t>(pos);
|
||||
auto const p = std::find(b, grid_.end(), empty);
|
||||
return p - grid_.begin();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<T> grid_; ///< The grid
|
||||
size_t length_; ///< Side length
|
||||
|
||||
static constexpr char empty = 0; ///< Character used for an empty cell.
|
||||
static constexpr char filled = 1; ///< Character used for a filled cell,
|
||||
};
|
||||
|
||||
/** Get the n-th triangular number. */
|
||||
auto triangle_num(size_t n) noexcept -> size_t { return (n * (n + 1)) / 2; }
|
||||
|
||||
@@ -245,112 +154,155 @@ namespace {
|
||||
size_t completed_tasks = 0;
|
||||
};
|
||||
|
||||
/** Search directly for a solution to the \a n th Partridge problem.
|
||||
*
|
||||
* Returns the grid of the solution.
|
||||
*/
|
||||
enum class CandidateOrder {
|
||||
ascending,
|
||||
descending,
|
||||
};
|
||||
|
||||
/** A maximal level skyline segment which is lower than its neighbours. */
|
||||
struct Valley {
|
||||
size_t x;
|
||||
size_t height;
|
||||
size_t width;
|
||||
};
|
||||
|
||||
/** Return the narrowest local valley, breaking ties by height then x. */
|
||||
[[nodiscard]] auto smallest_valley(std::vector<size_t> const &skyline) noexcept
|
||||
-> Valley {
|
||||
Valley best{0, 0, skyline.size()};
|
||||
bool found = false;
|
||||
for (size_t begin = 0; begin < skyline.size();) {
|
||||
auto end = begin + 1;
|
||||
while (end < skyline.size() && skyline[end] == skyline[begin]) {
|
||||
++end;
|
||||
}
|
||||
auto const left_height =
|
||||
begin == 0 ? skyline.size() : skyline[begin - 1];
|
||||
auto const right_height =
|
||||
end == skyline.size() ? skyline.size() : skyline[end];
|
||||
auto const lower_than_left = skyline[begin] < left_height;
|
||||
auto const lower_than_right = skyline[begin] < right_height;
|
||||
auto const width = end - begin;
|
||||
if (lower_than_left && lower_than_right &&
|
||||
(!found || width < best.width ||
|
||||
(width == best.width && skyline[begin] < best.height) ||
|
||||
(width == best.width && skyline[begin] == best.height &&
|
||||
begin < best.x))) {
|
||||
best = {begin, skyline[begin], width};
|
||||
found = true;
|
||||
}
|
||||
begin = end;
|
||||
}
|
||||
assert(found);
|
||||
return best;
|
||||
}
|
||||
|
||||
template<bool Instrument>
|
||||
auto search_solution_impl(size_t const n, SearchCounters *const counters) noexcept
|
||||
-> Results {
|
||||
/* Implementation is iterative, as opposed to recursive.
|
||||
*
|
||||
* The recursive implementation is easier to understand - but is
|
||||
* slightly slower because of the repeated function calls (and
|
||||
* entry/exit).
|
||||
*
|
||||
* The basic algorithm is to start at the origin of the grid we
|
||||
* want to place squares on and iterate over the permutations of
|
||||
* available squares until we find one that fits.
|
||||
*/
|
||||
|
||||
// grid is our in-progress grid of square positions.
|
||||
auto const length = triangle_num(n);
|
||||
Grid grid(length);
|
||||
|
||||
/* avail_sqs is a vector indexed by square length indicating how many
|
||||
* squares are available. Initially set up so that avail_sqs[i] = i.
|
||||
*/
|
||||
Avail avail_sqs;
|
||||
for (auto i = 0; i <= n; ++i) { avail_sqs.push_back(i); }
|
||||
|
||||
/* sqs is a vector used as a stack of the squares currently placed.
|
||||
* We reserve the length we need so as not to have too many allocations.
|
||||
*/
|
||||
std::vector<Square> sqs;
|
||||
sqs.reserve(length);
|
||||
|
||||
// Start at the origin with a square of longest side length.
|
||||
Pos pos = 0;
|
||||
size_t idx = n;
|
||||
|
||||
auto search_skyline(size_t const n, size_t const length,
|
||||
CandidateOrder const candidate_order,
|
||||
std::vector<size_t> &skyline, Avail &available,
|
||||
std::vector<Square> &squares,
|
||||
SearchCounters *const counters) noexcept -> bool {
|
||||
if constexpr (Instrument) {
|
||||
assert(counters != nullptr);
|
||||
++counters->search_nodes;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (squares.size() == length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto const valley = smallest_valley(skyline);
|
||||
auto const largest =
|
||||
std::min({n, valley.width, length - valley.height});
|
||||
auto try_side = [&](size_t const side) {
|
||||
if constexpr (Instrument) {
|
||||
++counters->loop_iterations;
|
||||
}
|
||||
/* If the idx is 0 we've looked at all possible square lengths for this
|
||||
* position, and they've failed. Pop the last square of the stack, remove
|
||||
* it from the grid and try the next smaller size in the same position.
|
||||
*/
|
||||
if (idx == 0) {
|
||||
// No squares on the stack -> failed to find a solution.
|
||||
if (sqs.empty()) { break; }
|
||||
|
||||
auto sq = sqs.back();
|
||||
sqs.pop_back();
|
||||
grid.clear(sq);
|
||||
++avail_sqs[sq.length()];
|
||||
if constexpr (Instrument) {
|
||||
++counters->backtracks;
|
||||
}
|
||||
pos = sq.pos();
|
||||
idx = sq.length() - 1;
|
||||
continue;
|
||||
if (available[side] == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are no squares available of the current size try the next one.
|
||||
if (avail_sqs[idx] == 0) {
|
||||
--idx;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Place a square of side length idx at pos, push this onto the stack and
|
||||
* set up to look at the next position.
|
||||
*/
|
||||
auto const sq = Square(pos, idx);
|
||||
if constexpr (Instrument) {
|
||||
++counters->attempted_placements;
|
||||
}
|
||||
--avail_sqs[idx];
|
||||
grid.add(sq);
|
||||
sqs.push_back(sq);
|
||||
--available[side];
|
||||
std::fill_n(skyline.begin() + static_cast<std::ptrdiff_t>(valley.x),
|
||||
side, valley.height + side);
|
||||
squares.emplace_back(valley.x + valley.height * length, side);
|
||||
|
||||
pos = grid.next_pos(pos + idx);
|
||||
|
||||
// Have we reached the end? If so success!
|
||||
if (pos == grid.end()) { break; }
|
||||
|
||||
if constexpr (Instrument) {
|
||||
++counters->search_nodes;
|
||||
if (search_skyline<Instrument>(n, length, candidate_order, skyline,
|
||||
available, squares, counters)) {
|
||||
return true;
|
||||
}
|
||||
idx = grid.largest_square(pos, n);
|
||||
}
|
||||
|
||||
return {length, sqs};
|
||||
squares.pop_back();
|
||||
std::fill_n(skyline.begin() + static_cast<std::ptrdiff_t>(valley.x),
|
||||
side, valley.height);
|
||||
++available[side];
|
||||
if constexpr (Instrument) {
|
||||
++counters->backtracks;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (candidate_order == CandidateOrder::ascending) {
|
||||
for (size_t side = 1; side <= largest; ++side) {
|
||||
if (try_side(side)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto side = largest; side != 0; --side) {
|
||||
if (try_side(side)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto search_solution(size_t const n) noexcept -> Results {
|
||||
return search_solution_impl<false>(n, nullptr);
|
||||
/** Search directly for a solution to the \a n th Partridge problem.
|
||||
*
|
||||
* The state is one filled height per board column. At each node the
|
||||
* narrowest local valley is found by a linear scan and candidates are tried
|
||||
* at its far-left edge. A placement or undo touches one entry per square
|
||||
* column. Scanning the profile costs O(board width); trying up to n
|
||||
* candidates and updating up to n columns for each costs O(n^2), for
|
||||
* O(board width + n^2) local work per node and the same total state.
|
||||
*/
|
||||
template<bool Instrument>
|
||||
auto search_solution_impl(size_t const n, CandidateOrder const candidate_order,
|
||||
SearchCounters *const counters) noexcept
|
||||
-> Results {
|
||||
auto const length = triangle_num(n);
|
||||
std::vector<size_t> skyline(length);
|
||||
Avail available(n + 1);
|
||||
for (size_t side = 0; side <= n; ++side) {
|
||||
available[side] = side;
|
||||
}
|
||||
std::vector<Square> squares;
|
||||
squares.reserve(length);
|
||||
static_cast<void>(search_skyline<Instrument>(
|
||||
n, length, candidate_order, skyline, available, squares, counters));
|
||||
|
||||
return {length, std::move(squares)};
|
||||
}
|
||||
|
||||
auto search_solution(
|
||||
size_t const n,
|
||||
CandidateOrder const candidate_order = CandidateOrder::ascending) noexcept
|
||||
-> Results {
|
||||
return search_solution_impl<false>(n, candidate_order, nullptr);
|
||||
}
|
||||
|
||||
auto search_solution_instrumented(size_t const n,
|
||||
SearchCounters &counters) noexcept -> Results {
|
||||
SearchCounters &counters,
|
||||
CandidateOrder const candidate_order =
|
||||
CandidateOrder::ascending) noexcept
|
||||
-> Results {
|
||||
counters = {};
|
||||
return search_solution_impl<true>(n, &counters);
|
||||
return search_solution_impl<true>(n, candidate_order, &counters);
|
||||
}
|
||||
|
||||
/** Construct an odd-order solution from its even-order predecessor. */
|
||||
|
||||
Reference in New Issue
Block a user