Pangolin
Header-only C++20 plane computational geometry library
Loading...
Searching...
No Matches
polyominoes.hpp
Go to the documentation of this file.
1#pragma once
2
4#include <cstdint>
5
16
17#include <algorithm>
18#include <array>
19#include <cstddef>
20#include <map>
21#include <set>
22#include <utility>
23#include <vector>
24
25namespace pgl {
26
27namespace detail {
28
30using PolyCell = std::pair<int, int>;
32using CellSet = std::vector<PolyCell>;
33
35inline constexpr std::array<PolyCell, 4> polyNeighbors{
36 PolyCell{1, 0}, PolyCell{-1, 0}, PolyCell{0, 1}, PolyCell{0, -1}};
37
39inline CellSet normalizeCells(CellSet cells) {
40 if (cells.empty()) {
41 return cells;
42 }
43 int minX = cells.front().first;
44 int minY = cells.front().second;
45 for (const auto& [x, y] : cells) {
46 minX = std::min(minX, x);
47 minY = std::min(minY, y);
48 }
49 for (auto& [x, y] : cells) {
50 x -= minX;
51 y -= minY;
52 }
53 std::sort(cells.begin(), cells.end());
54 return cells;
55}
56
64inline std::vector<CellSet> fixedPolyominoes(int n) {
65 if (n <= 0) {
66 return {};
67 }
68 std::set<CellSet> current{normalizeCells({{0, 0}})};
69 for (int k = 1; k < n; ++k) {
70 std::set<CellSet> next;
71 for (const auto& poly : current) {
72 const std::set<PolyCell> occupied(poly.begin(), poly.end());
73 for (const auto& cell : poly) {
74 for (const auto& [dx, dy] : polyNeighbors) {
75 const PolyCell candidate{cell.first + dx, cell.second + dy};
76 if (occupied.count(candidate)) {
77 continue;
78 }
79 CellSet grown = poly;
80 grown.push_back(candidate);
81 next.insert(normalizeCells(std::move(grown)));
82 }
83 }
84 }
85 current.swap(next);
86 }
87 return {current.begin(), current.end()};
88}
89
92inline CellSet transformCells(const CellSet& cells, int symmetry) {
93 CellSet out;
94 out.reserve(cells.size());
95 for (auto [x, y] : cells) {
96 if (symmetry & 1) {
97 x = -x;
98 }
99 for (int rot = symmetry >> 1; rot > 0; --rot) {
100 const int nx = -y;
101 const int ny = x;
102 x = nx;
103 y = ny;
104 }
105 out.push_back({x, y});
106 }
107 return normalizeCells(std::move(out));
108}
109
118inline bool polyominoHasHole(const CellSet& cells) {
119 if (cells.empty()) {
120 return false;
121 }
122 int maxX = 0, maxY = 0;
123 for (const auto& [x, y] : cells) {
124 maxX = std::max(maxX, x);
125 maxY = std::max(maxY, y);
126 }
127 const std::set<PolyCell> occupied(cells.begin(), cells.end());
128
129 // Flood the exterior, padded by one cell so it surrounds the whole box.
130 const auto inside = [&](int x, int y) {
131 return x >= -1 && x <= maxX + 1 && y >= -1 && y <= maxY + 1;
132 };
133 std::set<PolyCell> reached{{-1, -1}};
134 std::vector<PolyCell> stack{{-1, -1}};
135 while (!stack.empty()) {
136 const auto [x, y] = stack.back();
137 stack.pop_back();
138 for (const auto& [dx, dy] : polyNeighbors) {
139 const PolyCell next{x + dx, y + dy};
140 if (!inside(next.first, next.second) || occupied.count(next) ||
141 reached.count(next)) {
142 continue;
143 }
144 reached.insert(next);
145 stack.push_back(next);
146 }
147 }
148
149 for (int x = 0; x <= maxX; ++x) {
150 for (int y = 0; y <= maxY; ++y) {
151 const PolyCell cell{x, y};
152 if (!occupied.count(cell) && !reached.count(cell)) {
153 return true;
154 }
155 }
156 }
157 return false;
158}
159
162inline CellSet canonicalFree(const CellSet& cells) {
163 CellSet best = transformCells(cells, 0);
164 for (int symmetry = 1; symmetry < 8; ++symmetry) {
165 CellSet candidate = transformCells(cells, symmetry);
166 if (candidate < best) {
167 best = std::move(candidate);
168 }
169 }
170 return best;
171}
172
189inline std::vector<std::vector<PolyCell>> polyominoLoops(const CellSet& cells) {
190 const std::set<PolyCell> occupied(cells.begin(), cells.end());
191
192 // Directed boundary edges keyed by source vertex; interior on the left.
193 std::map<PolyCell, std::vector<PolyCell>> outgoing;
194 for (const auto& [x, y] : cells) {
195 if (!occupied.count({x, y - 1})) {
196 outgoing[{x, y}].push_back({x + 1, y}); // bottom, heading east
197 }
198 if (!occupied.count({x + 1, y})) {
199 outgoing[{x + 1, y}].push_back({x + 1, y + 1}); // right, heading north
200 }
201 if (!occupied.count({x, y + 1})) {
202 outgoing[{x + 1, y + 1}].push_back({x, y + 1}); // top, heading west
203 }
204 if (!occupied.count({x - 1, y})) {
205 outgoing[{x, y + 1}].push_back({x, y}); // left, heading south
206 }
207 }
208
209 std::set<std::pair<PolyCell, PolyCell>> used;
210 std::vector<std::vector<PolyCell>> loops;
211 for (const auto& [source, targets] : outgoing) {
212 for (const PolyCell& first : targets) {
213 if (used.count({source, first})) {
214 continue;
215 }
216 used.insert({source, first});
217 std::vector<PolyCell> loop{source};
218 int dx = first.first - source.first;
219 int dy = first.second - source.second;
220 PolyCell current = first;
221
222 // The preference gives every directed edge one successor and every
223 // vertex as many outgoing edges as incoming, so the successors of
224 // the edges leaving a vertex are distinct: following them from an
225 // unused edge walks a whole loop and stops back at its source.
226 while (current != source) {
227 loop.push_back(current);
228 // Preference order: sharpest right, straight, left, then reverse.
229 const std::array<PolyCell, 4> preferred{
230 PolyCell{dy, -dx}, PolyCell{dx, dy}, PolyCell{-dy, dx},
231 PolyCell{-dx, -dy}};
232 const std::vector<PolyCell>& edges = outgoing.at(current);
233 for (const auto& [pdx, pdy] : preferred) {
234 const PolyCell target{current.first + pdx, current.second + pdy};
235 if (std::find(edges.begin(), edges.end(), target) == edges.end()) {
236 continue;
237 }
238 used.insert({current, target});
239 dx = pdx;
240 dy = pdy;
241 current = target;
242 break;
243 }
244 }
245 loops.push_back(std::move(loop));
246 }
247 }
248 return loops;
249}
250
253inline int64_t loopTwiceArea(const std::vector<PolyCell>& loop) {
254 int64_t twice = 0;
255 const std::size_t m = loop.size();
256 for (std::size_t i = 0; i < m; ++i) {
257 const PolyCell here = loop[i];
258 const PolyCell next = loop[(i + 1) % m];
259 twice += static_cast<int64_t>(here.first) * next.second -
260 static_cast<int64_t>(next.first) * here.second;
261 }
262 return twice;
263}
264
266template <class T>
267std::vector<Point<T>> loopCorners(const std::vector<PolyCell>& loop) {
268 std::vector<Point<T>> corners;
269 const std::size_t m = loop.size();
270 for (std::size_t i = 0; i < m; ++i) {
271 const PolyCell prev = loop[(i + m - 1) % m];
272 const PolyCell here = loop[i];
273 const PolyCell next = loop[(i + 1) % m];
274 const int64_t cross =
275 static_cast<int64_t>(here.first - prev.first) * (next.second - here.second) -
276 static_cast<int64_t>(here.second - prev.second) * (next.first - here.first);
277 if (cross != 0) {
278 corners.emplace_back(static_cast<T>(here.first), static_cast<T>(here.second));
279 }
280 }
281 return corners;
282}
283
291template <class T>
292std::vector<Point<T>> polyominoOutline(const CellSet& cells) {
293 for (const auto& loop : polyominoLoops(cells)) {
294 if (loopTwiceArea(loop) > 0) {
295 return loopCorners<T>(loop);
296 }
297 }
298 return {};
299}
300
310template <class T>
311PolygonWithHoles<Point<T>> polyominoRegion(const CellSet& cells) {
312 Polygon<Point<T>> outer;
313 std::vector<Polygon<Point<T>>> holes;
314 for (const auto& loop : polyominoLoops(cells)) {
315 if (loopTwiceArea(loop) > 0) {
316 outer = Polygon<Point<T>>(loopCorners<T>(loop));
317 } else {
318 holes.emplace_back(loopCorners<T>(loop));
319 }
320 }
321 return PolygonWithHoles<Point<T>>(std::move(outer), std::move(holes));
322}
323
324} // namespace detail
325
343template <class T = int>
344std::vector<Polygon<Point<T>>> polyominoes(std::size_t size) {
345 std::vector<Polygon<Point<T>>> result;
346 if (size == 0) {
347 return result;
348 }
349
350 std::set<detail::CellSet> freeForms;
351 for (const auto& fixed : detail::fixedPolyominoes(static_cast<int>(size))) {
352 freeForms.insert(detail::canonicalFree(fixed));
353 }
354
355 for (const auto& form : freeForms) {
356 if (detail::polyominoHasHole(form)) {
357 continue;
358 }
359 result.emplace_back(detail::polyominoOutline<T>(form));
360 }
361 return result;
362}
363
375template <class T = int>
376std::vector<Polygon<Point<T>>> polyominoes(std::size_t n1, std::size_t n2) {
377 std::vector<Polygon<Point<T>>> result;
378 for (std::size_t size = n1; size <= n2; ++size) {
379 std::vector<Polygon<Point<T>>> sized = polyominoes<T>(size);
380 result.insert(result.end(), std::make_move_iterator(sized.begin()),
381 std::make_move_iterator(sized.end()));
382 }
383 return result;
384}
385
396template <class T = int>
397std::vector<Polygon<Point<T>>> polyominoesUpTo(std::size_t n) {
398 return polyominoes<T>(1, n);
399}
400
419template <class T = int>
420std::vector<PolygonWithHoles<Point<T>>> polyominoRegions(std::size_t size) {
421 std::vector<PolygonWithHoles<Point<T>>> result;
422 if (size == 0) {
423 return result;
424 }
425
426 std::set<detail::CellSet> freeForms;
427 for (const auto& fixed : detail::fixedPolyominoes(static_cast<int>(size))) {
428 freeForms.insert(detail::canonicalFree(fixed));
429 }
430
431 for (const auto& form : freeForms) {
432 result.push_back(detail::polyominoRegion<T>(form));
433 }
434 return result;
435}
436
448template <class T = int>
449std::vector<PolygonWithHoles<Point<T>>> polyominoRegions(std::size_t n1, std::size_t n2) {
450 std::vector<PolygonWithHoles<Point<T>>> result;
451 for (std::size_t size = n1; size <= n2; ++size) {
452 std::vector<PolygonWithHoles<Point<T>>> sized = polyominoRegions<T>(size);
453 result.insert(result.end(), std::make_move_iterator(sized.begin()),
454 std::make_move_iterator(sized.end()));
455 }
456 return result;
457}
458
469template <class T = int>
470std::vector<PolygonWithHoles<Point<T>>> polyominoRegionsUpTo(std::size_t n) {
471 return polyominoRegions<T>(1, n);
472}
473
474} // namespace pgl
Definition arrangement.hpp:67
@ y
Definition intervaltree.hpp:24
@ x
Definition intervaltree.hpp:24
std::vector< Polygon< Point< T > > > polyominoes(std::size_t size)
Enumerates the free polyominoes of a given size as polygons.
Definition polyominoes.hpp:344
PolygonWithHoles() -> PolygonWithHoles< Point<>, NoLabel >
Definition polygonwithholes.hpp:3093
std::vector< PolygonWithHoles< Point< T > > > polyominoRegionsUpTo(std::size_t n)
Enumerates the free polyominoes of every size from 1 to n as regions.
Definition polyominoes.hpp:470
std::vector< Polygon< Point< T > > > polyominoesUpTo(std::size_t n)
Enumerates the free polyominoes of every size from 1 to n.
Definition polyominoes.hpp:397
std::vector< PolygonWithHoles< Point< T > > > polyominoRegions(std::size_t size)
Enumerates the free polyominoes of a given size as regions.
Definition polyominoes.hpp:420
Polygon() -> Polygon< Point<>, NoLabel >
Definition polygon.hpp:3200
Angular sorting of points around a center.