From f5021aca301d051a40f064cd7f361c4836afb4ec Mon Sep 17 00:00:00 2001 From: Matthew Gretton-Dann Date: Tue, 2 Sep 2025 19:11:01 +0200 Subject: [PATCH] Change implementation to iterative. 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. --- main.cc | 473 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 252 insertions(+), 221 deletions(-) diff --git a/main.cc b/main.cc index 25b08a5..199c88f 100644 --- a/main.cc +++ b/main.cc @@ -13,243 +13,274 @@ #include #include -/** (x, y) pair storing a position. */ -using Pos = std::pair; +namespace { + /** (x, y) pair storing a position. */ + using Pos = std::pair; -/** Get x co-ordinate from position */ -auto x(Pos const &p) noexcept -> int { return p.first; } + /** Get x co-ordinate from position */ + auto x(Pos const &p) noexcept -> int { return p.first; } -/** Get y co-ordinate from position */ -auto y(Pos const &p) noexcept -> int { return p.second; } + /** Get y co-ordinate from position */ + auto y(Pos const &p) noexcept -> int { return p.second; } -/** A square - consisting of position of closest corner to origin, and side-length. - */ -struct Square { - /** Construct a square. - * \param pos Position of closest corner to origin - * \param length Side length. + /** A square - consisting of position of closest corner to origin, and side-length. */ - Square(Pos const &pos, int const length) noexcept : pos_(pos), length_(length) { - } - - Square(Square const &other) noexcept = default; - - Square(Square &&other) noexcept = default; - - Square &operator=(Square const &other) noexcept = default; - - Square &operator=(Square &&other) noexcept = default; - - ~Square() noexcept = default; - - /** Get x co-ordinate of closest corner to origin. */ - auto x() const noexcept -> int { return ::x(pos_); } - - /** Get y co-ordinate of closest corner to origin. */ - auto y() const noexcept -> int { return ::y(pos_); } - - /** Get side length. */ - auto length() const noexcept -> int { return length_; } - -private: - Pos pos_; ///< Position of corner closest to origin - int length_; ///< Side length -}; - -/** An N * N grid of characters. */ -struct Grid { - /** Construct a grid of given side-length. */ - Grid(int 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 */ - auto length() const noexcept -> int { return length_; } - - /** Add a square to the grid. */ - auto add(Square const &sq) noexcept -> void { - for (auto i = sq.x(); i < sq.x() + sq.length(); ++i) { - for (auto j = sq.y(); j < sq.y() + sq.length(); ++j) { - set(i, j, sq.length()); - } - } - } - - /** Clear a square from the grid. */ - auto clear(Square const &sq) noexcept -> void { - for (auto i = sq.x(); i < sq.x() + sq.length(); ++i) { - for (auto j = sq.y(); j < sq.y() + sq.length(); ++j) { - set(i, j, empty); - } - } - } - - auto prettify() noexcept -> void { - for (auto idx = 0; idx < grid_.size(); ++idx) { - if (grid_[idx] < 32) { - auto const pos = std::make_pair(idx % length_, idx / length_); - prettify_sq(Square(pos, grid_[idx])); - } - } - } - - /** Output the grid. */ - auto output() const -> void { - for (auto idx = 0; idx < length_ * length_; idx += length_) { - std::cout << std::string_view(grid_.data() + idx, length_) << '\n'; - } - } - - /** \brief Get length of the largest square that fits at \a pos in the grid. - */ - auto largest_square(Pos const &pos, int n) const noexcept -> int { - assert(x(pos) < length_); - assert(y(pos) < length_); - - /* 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. + struct Square { + /** Construct a square. + * \param pos Position of closest corner to origin + * \param length Side length. */ - 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; + Square(Pos const &pos, int const length) noexcept : pos_(pos), length_(length) { } - // Check that this length fits vertically as well. - auto len = b - x(pos); - auto ye = std::min(y(pos) + len, length_); - return ye - y(pos); - } - /** Get the next position to check. n is the size of the square we just - * added. - */ - auto next_pos(Pos const &pos, int n) const noexcept -> Pos { - auto const b = grid_.begin() + x(pos) + n + y(pos) * length_; - auto const p = std::find(b, grid_.end(), empty); - auto const v = p - grid_.begin(); - return std::make_pair(v % length_, v / length_); - } + Square(Square const &other) noexcept = default; -private: - auto prettify_sq(Square const &sq) noexcept -> void { - switch (sq.length()) { - case 1: set(sq.x(), sq.y(), '*'); - break; - 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, '+'); - break; - default: { - auto n = sq.length(); - set(sq.x(), sq.y(), '+'); - set(sq.x() + n - 1, sq.y(), '+'); - set(sq.x(), sq.y() + n - 1, '+'); - set(sq.x() + n - 1, sq.y() + n - 1, '+'); - for (int i = 1; i < n - 1; ++i) { - set(sq.x() + i, sq.y(), '-'); - set(sq.x() + i, sq.y() + n - 1, '-'); - set(sq.x(), sq.y() + i, '|'); - for (int j = 1; j < n - 1; ++j) { - set(sq.x() + j, sq.y() + i, ' '); - } - set(sq.x() + n - 1, sq.y() + i, '|'); - } + Square(Square &&other) noexcept = default; - int i = sq.x() + n - 1; - while (n != 0) { - set(--i, sq.y() + 1, '0' + (n % 10)); - n /= 10; + Square &operator=(Square const &other) noexcept = default; + + Square &operator=(Square &&other) noexcept = default; + + ~Square() noexcept = default; + + /** Get x co-ordinate of closest corner to origin. */ + auto x() const noexcept -> int { return ::x(pos_); } + + /** Get y co-ordinate of closest corner to origin. */ + auto y() const noexcept -> int { return ::y(pos_); } + + /** Get side length. */ + auto length() const noexcept -> int { return length_; } + + private: + Pos pos_; ///< Position of corner closest to origin + int length_; ///< Side length + }; + + /** An N * N grid of characters. */ + struct Grid { + /** Construct a grid of given side-length. */ + Grid(int 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 */ + auto length() const noexcept -> int { return length_; } + + /** Add a square to the grid. */ + auto add(Square const &sq) noexcept -> void { + for (auto i = sq.x(); i < sq.x() + sq.length(); ++i) { + for (auto j = sq.y(); j < sq.y() + sq.length(); ++j) { + set(i, j, sq.length()); } } } - } - /** Set the grid position (x, y) to the character c. + /** Clear a square from the grid. */ + auto clear(Square const &sq) noexcept -> void { + for (auto i = sq.x(); i < sq.x() + sq.length(); ++i) { + for (auto j = sq.y(); j < sq.y() + sq.length(); ++j) { + set(i, j, empty); + } + } + } + + auto prettify() noexcept -> void { + for (auto idx = 0; idx < grid_.size(); ++idx) { + if (grid_[idx] < 32) { + auto const pos = std::make_pair(idx % length_, idx / length_); + prettify_sq(Square(pos, grid_[idx])); + } + } + } + + /** Output the grid. */ + auto output() const -> void { + for (auto idx = 0; idx < length_ * length_; idx += length_) { + std::cout << std::string_view(grid_.data() + idx, length_) << '\n'; + } + } + + /** \brief Get length of the largest square that fits at \a pos in the grid. + */ + auto largest_square(Pos const &pos, int n) const noexcept -> int { + assert(x(pos) < length_); + assert(y(pos) < length_); + + /* 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 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; + } + // Check that this length fits vertically as well. + auto len = b - x(pos); + auto ye = std::min(y(pos) + len, length_); + return ye - y(pos); + } + + /** Get the next position to check. n is the size of the square we just + * added. + */ + auto next_pos(Pos const &pos, int n) const noexcept -> Pos { + auto const b = grid_.begin() + x(pos) + n + y(pos) * length_; + auto const p = std::find(b, grid_.end(), empty); + auto const v = p - grid_.begin(); + return std::make_pair(v % length_, v / length_); + } + + private: + auto prettify_sq(Square const &sq) noexcept -> void { + switch (sq.length()) { + case 1: set(sq.x(), sq.y(), '*'); + break; + 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, '+'); + break; + default: { + auto n = sq.length(); + set(sq.x(), sq.y(), '+'); + set(sq.x() + n - 1, sq.y(), '+'); + set(sq.x(), sq.y() + n - 1, '+'); + set(sq.x() + n - 1, sq.y() + n - 1, '+'); + for (int i = 1; i < n - 1; ++i) { + set(sq.x() + i, sq.y(), '-'); + set(sq.x() + i, sq.y() + n - 1, '-'); + set(sq.x(), sq.y() + i, '|'); + for (int j = 1; j < n - 1; ++j) { + set(sq.x() + j, sq.y() + i, ' '); + } + set(sq.x() + n - 1, sq.y() + i, '|'); + } + + int i = sq.x() + n - 1; + while (n != 0) { + set(--i, sq.y() + 1, '0' + (n % 10)); + n /= 10; + } + } + } + } + + /** Set the grid position (x, y) to the character c. + * + * It is an error if (x, y) is already set to c - as that means we have overlapping + * squares. + */ + auto set(int x, int y, char c) noexcept -> void { + assert(x < length_); + assert(y < length_); + assert(grid_[x + y * length_] != c); + grid_[x + y * length_] = c; + } + + std::vector grid_; ///< The grid + int length_; ///< Side length + + static constexpr char empty = '.'; ///< Character used for an empty cell. + }; + + /** Get the n'th triangular number. */ + auto triangle_num(int n) noexcept -> int { return (n * (n + 1)) / 2; } + + /** Vector used to identify the available squares. */ + using Avail = std::vector; + + /** Find a solution to the \a n th Partridge problem. * - * It is an error if (x, y) is already set to c - as that means we have overlapping - * squares. + * Returns the grid of the solution. */ - auto set(int x, int y, char c) noexcept -> void { - assert(x < length_); - assert(y < length_); - assert(grid_[x + y * length_] != c); - grid_[x + y * length_] = c; + auto find_solution(int const n) noexcept -> Grid { + /* 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 inprogress 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 sqs; + sqs.reserve(length); + + // Start at the origin with a square of longest side length. + Pos pos{0, 0}; + int idx = n; + + while (true) { + /* 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 one 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()]; + pos = std::make_pair(sq.x(), sq.y()); + idx = sq.length() - 1; + continue; + } + + // 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); + --avail_sqs[idx]; + grid.add(sq); + sqs.push_back(sq); + + pos = grid.next_pos(pos, idx); + idx = grid.largest_square(pos, n); + + // Have we reached the end? If so success! + if (x(pos) == 0 && y(pos) == grid.length()) { break; } + } + + grid.prettify(); + return grid; } - - std::vector grid_; ///< The grid - int length_; ///< Side length - - static const int empty = '.'; ///< Character used for an empty cell. -}; - -/** Get the n'th triangular number. */ -auto triangle_num(int n) noexcept -> int { return (n * (n + 1)) / 2; } - -/** Vector used to identify the available squares. */ -using Avail = std::vector; - -/** Recursive part of solution finder. - * - * Fills \a grid with squares from \a avail_sqs, starting at the current - * position \a pos. \a n is the side-length of the largest square. - * - * Returns \c true if a solution is found, \a c false if not. - */ -auto find_solution_impl(Grid &grid, int n, Pos const &pos, Avail &avail_sqs) noexcept -> bool { - if (x(pos) == 0 && y(pos) == grid.length()) { return true; } - - /* 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; } - - auto const sq = Square(pos, idx); - --avail_sqs[idx]; - grid.add(sq); - if (find_solution_impl(grid, n, grid.next_pos(pos, idx), avail_sqs)) { return true; } - ++avail_sqs[idx]; - grid.clear(sq); - } - - return false; -} - -/** Find a solution to the partridge problem of size n. - * - * Returns the solution grid. - */ -auto find_solution(int n) -> Grid { - auto length = triangle_num(n); - Grid grid(length); - Avail avail_sqs; - for (auto i = 0; i <= n; ++i) { avail_sqs.push_back(i); } - find_solution_impl(grid, n, std::make_pair(0, 0), avail_sqs); - grid.prettify(); - return grid; -} +} // anon namespace int main(int argc, char **argv) { auto n = (argc == 1) ? 8 : atoi(argv[1]);