Optimize: if sq N fits then sq (N - 1) does too.

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.
This commit is contained in:
2025-09-01 10:34:24 +02:00
parent 6ac6f2af9b
commit 3095d38b8a

76
main.cc
View File

@@ -15,11 +15,17 @@ auto x(Pos const& p) noexcept -> int { return p.first; }
auto y(Pos const &p) noexcept -> int { return p.second; } auto y(Pos const &p) noexcept -> int { return p.second; }
struct Square { struct Square {
Square(Pos const& pos, int length) noexcept : pos_(pos), length_(length) {} Square(Pos const &pos, int length) noexcept : pos_(pos), length_(length) {
}
Square(Square const &other) noexcept = default; Square(Square const &other) noexcept = default;
Square(Square &&other) noexcept = default; Square(Square &&other) noexcept = default;
Square &operator=(Square const &other) noexcept = default; Square &operator=(Square const &other) noexcept = default;
Square &operator=(Square &&other) noexcept = default; Square &operator=(Square &&other) noexcept = default;
~Square() noexcept = default; ~Square() noexcept = default;
auto x() const noexcept -> int { return ::x(pos_); } auto x() const noexcept -> int { return ::x(pos_); }
@@ -30,17 +36,26 @@ struct Square {
int length_; int length_;
}; };
struct OverlappingSquares { Pos pos_; }; struct OverlappingSquares {
Pos pos_;
};
struct Grid { struct Grid {
Grid(int length) : grid_(length * length, '.'), length_(length) {} Grid(int length) : grid_(length * length, '.'), length_(length) {
}
Grid(Grid const &other) = default; Grid(Grid const &other) = default;
Grid(Grid &&other) noexcept = default; Grid(Grid &&other) noexcept = default;
Grid &operator=(Grid const &other) = default; Grid &operator=(Grid const &other) = default;
Grid &operator=(Grid &&other) noexcept = default; Grid &operator=(Grid &&other) noexcept = default;
~Grid() noexcept = default; ~Grid() noexcept = default;
auto length() const noexcept -> int { return length_; } auto length() const noexcept -> int { return length_; }
auto set(int x, int y, char c) noexcept -> void { auto set(int x, int y, char c) noexcept -> void {
assert(x < length_); assert(x < length_);
assert(y < length_); assert(y < length_);
@@ -56,9 +71,12 @@ struct Grid {
auto add(Square const &sq) -> void { auto add(Square const &sq) -> void {
switch (sq.length()) { switch (sq.length()) {
case 1: set(sq.x(), sq.y(), '*'); break; case 1: set(sq.x(), sq.y(), '*');
break;
case 2: set(sq.x(), sq.y(), '+'); case 2: set(sq.x(), sq.y(), '+');
set(sq.x() + 1, sq.y(), '+');set(sq.x(), sq.y() + 1, '+');set(sq.x() + 1, sq.y() + 1, '+'); set(sq.x() + 1, sq.y(), '+');
set(sq.x(), sq.y() + 1, '+');
set(sq.x() + 1, sq.y() + 1, '+');
break; break;
default: { default: {
auto n = sq.length(); auto n = sq.length();
@@ -99,21 +117,31 @@ struct Grid {
} }
} }
auto fits(Square const& sq) const noexcept -> bool { /** \brief Get length of the largest square that fits at \a pos in the grid.
assert (sq.x() < length_); */
assert (sq.y() < length_); auto largest_square(Pos const &pos, int n) const noexcept -> int {
if (sq.x() + sq.length() > length_) { return false; } assert(x(pos) < length_);
else if (sq.y() + sq.length() > length_) { return false; } assert(y(pos) < length_);
else {
auto pos = grid_.begin() + sq.x() + sq.y() * length_; /* Because of how we walk through the grid (starting at 0,0 then increasing
std::string_view v(pos, pos + sq.length()); * x followed by y) we can assume that if the position (b, y) is clear
return v.find_first_not_of('.') == std::string_view::npos; * (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 b = x(pos);
// Make sure we don't go looking in the next row.
auto e = std::min(x(pos) + n, length_);
while (b < e) {
if (grid_[b + y(pos) * length_] != '.') { break; }
++b;
} }
return b - x(pos);
} }
auto next_pos(Pos const &pos) const noexcept -> Pos { auto next_pos(Pos const &pos) const noexcept -> Pos {
if (const auto p = x(pos) + 1 + y(pos) * length_; p >= grid_.size()) { return std::make_pair(0, length_); } if (const auto p = x(pos) + 1 + y(pos) * length_; p >= grid_.size()) { return std::make_pair(0, length_); } else {
else {
const auto next_p = grid_.find('.', p); const auto next_p = grid_.find('.', p);
if (next_p == std::string::npos) { if (next_p == std::string::npos) {
return std::make_pair(0, length_); return std::make_pair(0, length_);
@@ -133,18 +161,25 @@ using Avail = std::vector<int>;
auto find_solution_impl(Grid &grid, int n, Pos const &pos, Avail &avail_sqs) -> bool { auto find_solution_impl(Grid &grid, int n, Pos const &pos, Avail &avail_sqs) -> bool {
if (x(pos) == 0 && y(pos) == grid.length()) { return true; } if (x(pos) == 0 && y(pos) == grid.length()) { return true; }
for (auto idx = n; idx != 0; --idx) { /* Walk through the possible squares from the largest that will fit down to 0.
* We don't want to walk up from 1 to the largest because we can show that a
* 1x1 piece will never fit along an edge (or one in from the edge) - as
* there is no other 1x1 piece to go alongside it). This means we know the
* first guess is definitely wrong if we start small.
*
* If we know a square of side length N will fit then we know a square of side length
* N - 1 will fit.
*/
for (auto idx = grid.largest_square(pos, n); idx != 0; --idx) {
if (avail_sqs[idx] == 0) { continue; } if (avail_sqs[idx] == 0) { continue; }
auto const sq = Square(pos, idx); auto const sq = Square(pos, idx);
if (grid.fits(sq)) {
--avail_sqs[idx]; --avail_sqs[idx];
grid.add(sq); grid.add(sq);
if (find_solution_impl(grid, n, grid.next_pos(pos), avail_sqs)) { return true; } if (find_solution_impl(grid, n, grid.next_pos(pos), avail_sqs)) { return true; }
++avail_sqs[idx]; ++avail_sqs[idx];
grid.clear(sq); grid.clear(sq);
} }
}
return false; return false;
} }
@@ -158,8 +193,7 @@ auto find_solution(int n) -> Grid{
return grid; return grid;
} }
int main(int argc, char** argv) int main(int argc, char **argv) {
{
auto n = (argc == 1) ? 8 : atoi(argv[1]); auto n = (argc == 1) ? 8 : atoi(argv[1]);
auto grid = find_solution(n); auto grid = find_solution(n);
std::cout << "Partridge problem " << n << " side length " << grid.length() << '\n'; std::cout << "Partridge problem " << n << " side length " << grid.length() << '\n';