exact_covers/lib.rs
1// The following doc comment is kept in sync with the README.md file. Please
2// run the `cargo sync-readme` command after modifying the comment contents.
3//! This crate provides implementations of D. E. Knuth and C. Solnon's
4//! algorithms for solving the exact cover problem with color controls.
5//!
6//! Suppose we're given a collection $\mathcal{O}$ of _options_, each of which is
7//! a set of _items_; the _exact cover_ problem is to find a subcollection
8//! $\mathcal{O}^\star\subseteq\mathcal{O}$ of options such that each item occurs
9//! in exactly one of $\mathcal{O}^\star$'s options. Knuth proposed a method that
10//! achieves this goal in the paper "Dancing Links", [arXiv:cs/0011047][dl] [cs.DS]
11//! (2000), whose title refers to a clever yet simple technique for deleting and
12//! restoring the nodes of a doubly linked list. His backtracking scheme, called
13//! _Algorithm X_, employs this "waltzing" of links to visit all exact covers
14//! with options $\mathcal{O}$ in a recursive, depth-first manner. [For further
15//! information, see Section 7.2.2.1 of [_The Art of Computer Programming_ **4B** (2022)][taocp4b],
16//! Part 2, 65–70.]
17//!
18//! A slight modification of Algorithm X solves the considerably more general
19//! problem in which items fall into one of two categories: _primary_ and _secondary_.
20//! Now the task is to find a subcollection $\mathcal{O}^\star\subseteq\mathcal{O}$
21//! of options that cover every primary item _exactly_ once, while covering every
22//! secondary item _at most_ once. The _exact covering with colors_ (XCC) problem
23//! arises if we go further and assign a _color_ to the secondary items of each
24//! option. Then we say two options are _compatible_ if their secondary items
25//! have matching colors, and we define a solution as a collection $\mathcal{O}^\star\subseteq\mathcal{O}$
26//! of mutually compatible options that cover every primary item exactly once.
27//! (In contrast to the uncolored case, a secondary item can occur in more than
28//! one of $\mathcal{O}^\star$'s options provided that their colors are compatible.)
29//!
30//! Fortunately the dancing links technique is also well suited to XCC problems.
31//! Knuth proved this when he introduced Algorithm C in Section 7.2.2.1 of
32//! [_TAOCP_ **4B**][taocp4b], part 2, pages 87–91; his new procedure extends
33//! Algorithm X to colors. In 2020, C. Solnon suggested using the sparse-set
34//! data structure of P. Briggs and L. Torczon [[_ACM Letters on Programming Languages and Systems_ **2** (1993)][sparsesets],
35//! 59–69] to store the database of currently active options and the list of
36//! items that need to be covered. Knuth prepared an implementation of this
37//! approach, called the "dancing cells" method, using the conventions of
38//! Algorithm C. Numerous benchmark tests for these two XCC solvers appear
39//! in Section 7.2.2.3 of [_TAOCP_ **4C** (June 25, 2024)][taocp4c], Pre-Fascicle
40//! 7A (preliminary draft), pages 64–65. To summarize the results of these
41//! experiments: there is no clear winner, and we don't yet know a rule for
42//! determining which method is best for a particular instance of XCC.
43//!
44//! This crate is a library of subroutines for color-controlled covering of
45//! $N_1\ge0$ primary items and $N_2\ge0$ secondary items in the Rust
46//! programming language. The following structures are its most important pieces:
47//! - [`DlSolver`] finds all solutions to an XCC problem. It implements
48//! a slightly modified form of Knuth's Algorithm 7.2.2.1C.
49//! - [`DcSolver`] adheres to the same input and output conventions as the
50//! previous structure, but it uses the dancing cells technique.
51//!
52//! Both implementations support option simplification via the removal of
53//! _blocking_ and _forcing_. [For a discussion of these preprocessing operations,
54//! see [Knuth, _TAOCP_ **4B** (2022)][taocp4b], Part 2, 108–111.]
55//!
56//! Also, the [`examples`] directory contains an instructive set of programs
57//! that apply these algorithms to a variety of problems:
58//! - [`langford_pairs.rs`] finds all [Langford pairings] of $2n$ numbers.
59//! - [`polycube_packing.rs`] computes the number of ways to arrange 25 [Y pentacubes]
60//! in a $5\times5\times5$ cuboid.
61//! - [`domino_chessboard.rs`] finds all ways to pack 32 dominoes into a chessboard.
62//!
63//! [dl]: https://arxiv.org/pdf/cs/0011047.pdf
64//! [taocp4b]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4
65//! [taocp4c]: https://cs.stanford.edu/~knuth/fasc7a.ps.gz
66//! [sparsesets]: https://dl.acm.org/doi/pdf/10.1145/176454.176484
67//! [`examples`]: https://github.com/hsanzg/exact-covers/tree/main/examples
68//! [`langford_pairs.rs`]: https://github.com/hsanzg/exact-covers/blob/main/examples/langford_pairs.rs
69//! [Langford pairings]: https://en.wikipedia.org/wiki/Langford_pairing
70//! [`polycube_packing.rs`]: https://github.com/hsanzg/exact-covers/blob/main/examples/polycube_packing.rs
71//! [Y pentacubes]: https://en.wikipedia.org/wiki/Polycube
72//! [`domino_chessboard.rs`]: https://github.com/hsanzg/exact-covers/blob/main/examples/domino_chessboard.rs
73
74mod dc;
75mod dl;
76mod indices;
77
78use std::marker::PhantomData;
79
80pub use dc::Solver as DcSolver;
81pub use dl::Solver as DlSolver;
82
83/// Visits all [solutions] to an XCC problem with $N_1\ge0$ primary items and
84/// $N_2\ge0$ secondary items.
85///
86/// See the [crate-level documentation](`crate`) for details.
87///
88/// # Notes
89///
90/// The solver does not visit any solution containing a purely secondary option
91/// (that is, an option that uses no primary items). However, the set of items
92/// covered by the options in any visited solution intersects every purely
93/// secondary option. This strategy is a generalized version of the "second
94/// death" method from exercise 7.2.2.1–19 of TAOCP to the XCC problem.
95///
96/// This trait is sealed, meaning that it cannot be implemented outside of the
97/// `exact-covers` crate.
98///
99/// # Example
100///
101/// Suppose we want to cover the primary items $a,b,c,d,e,f,g$ using some of
102/// the following options:
103/// \\[
104/// 'c\\;e';\quad'a\\;d\\;g';\quad'b\\;c\\;f';\quad'a\\;d\\;f';\quad'b\\;g';\quad'd\\;e\\;g'.
105/// \\]
106/// (D. E. Knuth posed this toy problem at the beginning of Section 7.2.2.1
107/// in [_The Art of Computer Programming_ **4B** (2022)][taocp4b], Part 2, page 66.)
108/// The following program uses an XCC solver based on the dancing links method
109/// to find the unique solution $'a\\;d\\;f';\\;'b\\;g';\\;'c\\;e'$:
110///
111/// ```
112/// use exact_covers::{DlSolver, Solver};
113///
114/// let items = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
115/// let mut solver: DlSolver<char, ()> = DlSolver::new(&items, &[]);
116/// solver.add_option([ 'c', 'e' ], []);
117/// solver.add_option(['a', 'd', 'g'], []);
118/// solver.add_option([ 'b', 'c', 'f' ], []);
119/// solver.add_option(['a', 'd', 'f' ], []);
120/// solver.add_option([ 'b', 'g'], []);
121/// solver.add_option([ 'd', 'e', 'g'], []);
122///
123/// // We use an auxiliary table to store the items of an option. The chief
124/// // purpose of this reserved storage is to reduce heap allocations when
125/// // constructing the solutions to an XCC problem.
126/// let mut option = Vec::new();
127/// solver.solve(|mut solution| {
128/// assert_eq!(solution.option_count(), 3);
129///
130/// assert!(solution.next(&mut option));
131/// assert_eq!(option, [&'a', &'d', &'f']);
132/// assert!(solution.next(&mut option));
133/// assert_eq!(option, [&'b', &'g']);
134/// assert!(solution.next(&mut option));
135/// assert_eq!(option, [&'c', &'e']);
136/// });
137/// ```
138///
139/// [solutions]: `Solution`
140/// [`add_option`]: Self::add_option
141/// [taocp4b]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4
142pub trait Solver<'i, I, C>: private::Solver<'i, I, C> {
143 /// Creates a solver for an XCC problem on the given primary and secondary
144 /// items.
145 ///
146 /// To specify the options to cover these items, use [`Self::add_option`].
147 fn new(primary: &'i [I], secondary: &'i [I]) -> Self;
148
149 /// Appends an option to the XCC problem.
150 ///
151 /// The meaning of a pair $(i,c)$ in the list of secondary items depends on
152 /// the value of $c$: If $c$ is `Some(c')` for some `c'`, then the secondary
153 /// item $i$ is assigned color `c'`; otherwise $i$ is implicitly assigned
154 /// a unique color, which does not match the color of the item in any other
155 /// option.
156 ///
157 /// Once all options have been specified, use [`Self::solve`] to visit all
158 /// solutions to the problem.
159 fn add_option<P, S>(&mut self, primary: P, secondary: S)
160 where
161 P: AsRef<[I]>,
162 S: AsRef<[(I, Option<C>)]>;
163
164 /// Calls a closure on each solution to the XCC problem.
165 fn solve<F>(self, visit: F)
166 where
167 F: FnMut(Solution<'_, 'i, I, C, Self>);
168}
169
170pub(crate) mod private {
171 use crate::indices::InstIndex;
172
173 pub trait Solver<'i, I, C> {
174 /// Returns the index of the [instance] in the option selected for
175 /// covering the first item covered at the given level of backtracking;
176 /// or [`None`], if the search tree is less than `level` levels deep.
177 ///
178 /// [instance]: crate::dl::Instance
179 fn pointer(&self, level: usize) -> Option<InstIndex>;
180
181 /// Returns the current level of the search.
182 fn level(&self) -> usize;
183
184 /// Constructs the option associated with a given [instance node] $x$,
185 /// starting with the item represented by $x$ and proceeding cyclically
186 /// from left to right.
187 ///
188 /// The resulting sequence of items replaces the previous contents
189 /// of `result`.
190 ///
191 /// # Panics
192 ///
193 /// This function panics if the node index is out of bounds.
194 ///
195 /// [instance node]: crate::dl::Node::Instance
196 fn option_of(&self, ix: InstIndex, result: &mut Vec<&'i I>);
197 }
198}
199
200/// An iterator over the options of a solution to an XCC problem.
201pub struct Solution<'s, 'i: 's, I, C, S> {
202 /// The solver that found the colored covering.
203 solver: &'s mut S,
204 /// The index of an element in `solver`'s [list of node pointers], which
205 /// corresponds to an ancestor of the present solution in the search tree.
206 /// The [`Self::next`] function uses this information to reconstruct the
207 /// option selected by the search algorithm at that level of recursion.
208 ///
209 /// [list of node pointers]: private::Solver::pointer
210 level: usize,
211 _phantom: PhantomData<(&'i I, C)>,
212}
213
214impl<'s, 'i, S, I, C> Solution<'s, 'i, I, C, S>
215where
216 S: Solver<'i, I, C>,
217 I: Eq,
218{
219 /// Places the items in the next option of the solution into `result`.
220 ///
221 /// Returns `false` and leaves the vector untouched if and only if
222 /// all options have already been enumerated.
223 pub fn next(&mut self, result: &mut Vec<&'i I>) -> bool {
224 if let Some(node_ix) = self.solver.pointer(self.level) {
225 // Update the recursion depth level and populate `result`.
226 self.level += 1;
227 self.solver.option_of(node_ix, result);
228 true
229 } else {
230 false
231 }
232 }
233
234 /// Returns the number of options in the solution.
235 pub fn option_count(&self) -> usize {
236 self.solver.level()
237 }
238}