polycube_packing/
polycube_packing.rs

1//! The following program finds all ways to pack 25 [Y pentacubes] into
2//! a $5\times5\times5$ box. (Exercise 340 in Section 7.2.2.1 of Knuth's
3//! [_The Art of Computer Programming_ **4B** (2022)][taocp4b], Part 2,
4//! illustrates the 29 pentacubes.) We formulate this task as an exact
5//! cover problem, with one item for each of the $125$ positions to cover,
6//! and one option for each [legal placement] of a piece. The program can
7//! be readily adapted to fill an arbitrary shape with a given set of
8//! polycubes.
9//!
10//! Chapter 8 of R. Honsberger's book [_Mathematical Gems II_][mgems] (1976)
11//! provides a good introduction to the techniques for solving polycube packing
12//! puzzles.
13//!
14//! [Y pentacube]: https://en.wikipedia.org/wiki/Polycube
15//! [taocp4b]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4
16//! [legal placement]: `is_in_bounds`
17//! [mgems]: https://bookstore.ams.org/dol-2
18
19use exact_covers::{DlSolver, Solver};
20use smallvec::SmallVec;
21use std::collections::HashSet;
22use std::iter;
23use std::ops::ControlFlow;
24
25/// A point in the cubic lattice.
26type Position = (i8, i8, i8);
27
28/// A solid object formed by joining $1\times 1\times 1$ cubies face to face.
29#[derive(Debug, Eq, PartialEq, Hash, Clone)]
30struct Polycube(
31    /// The positions occupied by the polycube.
32    SmallVec<Position, { Self::INLINE_CAP }>,
33);
34
35impl Polycube {
36    /// If the number of cubies in this polycube exceeds this threshold,
37    /// the buffer containing their positions is allocated on the heap.
38    const INLINE_CAP: usize = 5; // In this way the array of positions takes 15 bytes,
39                                 // which is one less than the space required
40                                 // for a 64-bit pointer and a capacity field.
41
42    /// Creates a polycube with one or more cubies, without copying any data.
43    ///
44    /// This is the `const` version of [`Polycube::from`].
45    pub const fn from_const(positions: [Position; Self::INLINE_CAP]) -> Self {
46        Self(SmallVec::from_buf(positions))
47    }
48
49    /// Applies a transformation to the polycube.
50    pub fn transform(&self, f: impl FnMut(Position) -> Position) -> Self {
51        Self(self.0.iter().copied().map(f).collect())
52    }
53
54    /// Returns the translate of the polycube whose cubie coordinates are
55    /// nonnegative and as small as possible.
56    ///
57    /// This shape is called the _aspect_ of the polycube. Section 1.3 of
58    /// the book [_Tilings and Patterns_][tap] by B. Grünbaum, G. C. Shephard
59    /// (W. H. Freeman, 1987) discusses the number of distinct aspects
60    /// of the tiles in particular classes of tessellations.
61    ///
62    /// [tap]: https://dl.acm.org/doi/10.5555/19304
63    pub fn aspect(&self) -> Self {
64        let x_min = self.0.iter().map(|(x, _, _)| x).min().unwrap();
65        let y_min = self.0.iter().map(|(_, y, _)| y).min().unwrap();
66        let z_min = self.0.iter().map(|(_, _, z)| z).min().unwrap();
67        self.transform(|(x, y, z)| (x - x_min, y - y_min, z - z_min))
68    }
69
70    /// Constructs the set of aspects corresponding to all 3D-rotations of
71    /// the polycube.
72    ///
73    /// D. E. Knuth called these the _base placements_ of a polycube in
74    /// exercise 7.2.2.1–266 of [_The Art of Computer Programming_ **4B**,
75    /// Part 2][taocp] (Addison-Wesley, 2022).
76    ///
77    /// [taocp]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4
78    pub fn base_placements(&self) -> HashSet<Polycube> {
79        // The group of symmetries of a polycube is isomorphic to a subgroup of
80        // the 24 3D-rotations of a cube, which is generated by the following
81        // two elementary transformations:
82        // 1. $90^\circ$ rotation about the $z$-axis, $(x,y,z)\mapsto(y,x_\text{max}-x,z)$.
83        // 2. Reflection about the $x=y=z$ diagonal: $(x,y,z)\mapsto(y,z,x)$.
84        // To see why, use the fact that the rotational symmetries of a cube
85        // form a group that is isomorphic to the symmetric group $S_4$ on 4
86        // elements. (Notice that the rotations of a cube permute its diagonals.)
87        // In this way transformations 1 and 2 correspond respectively to
88        // the permutations $\pi=(1234)$ and $\sigma=(142)$ under some
89        // appropriate naming of the cube vertices. And $\{\pi,\sigma\}$ is
90        // a generator of $S_4$.
91        let mut placements = HashSet::with_capacity(24);
92        let mut to_visit = Vec::with_capacity(8);
93        to_visit.push(self.aspect());
94        while let Some(shape) = to_visit.pop() {
95            let x_max = shape.0.iter().map(|(x, _, _)| x).max().unwrap();
96            let rotation = shape.transform(|(x, y, z)| (y, x_max - x, z));
97            if !placements.contains(&rotation) {
98                to_visit.push(rotation);
99            }
100
101            let reflection = shape.transform(|(x, y, z)| (y, z, x));
102            if !placements.contains(&reflection) {
103                to_visit.push(reflection);
104            }
105
106            placements.insert(shape);
107        }
108        placements
109    }
110
111    /// Returns the number of cubies in the polycube.
112    pub fn cubie_count(&self) -> usize {
113        self.0.len()
114    }
115}
116
117impl From<&[Position]> for Polycube {
118    /// Creates a polycube with one or more cubies.
119    fn from(positions: &[Position]) -> Self {
120        assert!(!positions.is_empty(), "a polycube has one or more cubies");
121        Self(SmallVec::from_slice(positions))
122    }
123}
124
125impl From<Vec<Position>> for Polycube {
126    /// Creates a polycube with one or more cubies.
127    fn from(positions: Vec<Position>) -> Self {
128        assert!(!positions.is_empty(), "a polycube has one or more cubies");
129        Self(positions.into())
130    }
131}
132
133/// The Y pentacube, with the vector normal to its bottom face—the face that is
134/// farthest-apart from the pentacube's center of mass—pointing downwards and
135/// the cubie not in the long bar appearing in the up-west position.
136const Y: Polycube = Polycube::from_const([(0, 2, 0), (1, 0, 0), (1, 1, 0), (1, 2, 0), (1, 3, 0)]);
137
138/// Returns `true` if and only if the given polycube lies inside the
139/// $l\times m\times n$ cuboid cornered at the origin.
140fn is_in_bounds(polycube: &Polycube, l: i8, m: i8, n: i8) -> bool {
141    let x_min = *polycube.0.iter().map(|(x, _, _)| x).min().unwrap();
142    let y_min = *polycube.0.iter().map(|(_, y, _)| y).min().unwrap();
143    let z_min = *polycube.0.iter().map(|(_, _, z)| z).min().unwrap();
144    let x_max = *polycube.0.iter().map(|(x, _, _)| x).max().unwrap();
145    let y_max = *polycube.0.iter().map(|(_, y, _)| y).max().unwrap();
146    let z_max = *polycube.0.iter().map(|(_, _, z)| z).max().unwrap();
147    0 <= x_min && x_max < l && 0 <= y_min && y_max < m && 0 <= z_min && z_max < n
148}
149
150fn main() {
151    // Define the items of the exact cover problem, namely the $lmn$ cells of
152    // the $l\times m\times n$ cuboid.
153    let (l, m, n) = (5i8, 5i8, 5i8);
154    let positions = (0..l)
155        .flat_map(|x| iter::repeat(x).zip(0..m))
156        .flat_map(|y| iter::repeat(y).zip(0..n))
157        .map(|((x, y), z)| (x, y, z))
158        .collect::<Vec<_>>();
159
160    let mut solver: DlSolver<Position, ()> = DlSolver::new(&positions, &[]);
161    // For each base placement $P$ of the Y pentacube, and for each offset
162    // $(x_0,y_0,z_0)$ such that the piece $P'=P+(x_0,y_0,z_0)$ lies within the
163    // $l\times m\times n$ cuboid cornered at the origin, define an option whose
164    // primary items are the cells of $P'$. We break symmetry by constraining
165    // a particular cubie of the first pentacube to be at $L_\infty$ distance
166    // $\le2$ from the origin.
167    let placements = Y.base_placements();
168    let mut first = true;
169    for placement in placements {
170        for (x_0, y_0, z_0) in &positions {
171            if first && (*x_0 > 2 || *y_0 > 2 || *z_0 > 2) {
172                continue;
173            }
174            let shifted = placement.transform(|(x, y, z)| (x + x_0, y + y_0, z + z_0));
175            if is_in_bounds(&shifted, l, m, n) {
176                solver.add_option(&shifted.0, []);
177            }
178        }
179        first = false;
180    }
181
182    let mut count = 0;
183    let mut polycube = Vec::with_capacity(Y.cubie_count());
184    solver.solve(|mut solution| {
185        // Print the solution, which consists of a set of 25 pentacubes.
186        print!("[");
187        while solution.next(&mut polycube) {
188            print!("[");
189            if let Some(((&last, _), elements)) = polycube.split_last() {
190                for (&cubie, _) in elements {
191                    print!("[{},{},{}],", cubie.0, cubie.1, cubie.2);
192                }
193                print!("[{},{},{}]", last.0, last.1, last.2);
194            }
195            print!("],");
196        }
197        println!("]");
198        count += 1;
199        ControlFlow::Continue(())
200    });
201    println!("found {count} packings");
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn y_base_placements() {
210        let placements = Y.base_placements();
211        assert_eq!(placements.len(), 24, "the Y pentacube has 24 3D-rotations");
212        for placement in &placements {
213            assert_eq!(
214                placement,
215                &placement.aspect(),
216                "a base placement is an aspect"
217            );
218        }
219    }
220}