Skip to content

Commit

Permalink
Beginning of exact covering with colors
Browse files Browse the repository at this point in the history
  • Loading branch information
hsanzg committed Jan 31, 2024
1 parent 3809528 commit 591888d
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 1,059 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/target
/benches
/old
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[package]
name = "exact-covers"
version = "0.1.1"
description = "A collection of exact cover solvers"
version = "0.2.0"
description = "An implementation of Knuth's algorithm for solving the exact cover problem with colors"
authors = ["Hugo Sanz González <hugo@hgsg.me>"]
license = "MIT"
homepage = "https://github.com/hsanzg/exact-covers"
documentation = "https://docs.rs/exact-covers"
repository = "https://github.com/hsanzg/exact-covers"
keywords = ["exact-cover", "dancing-links", "combinatorial-search", "polyomino-packing", "polycube-packing"]
keywords = ["exact-cover", "color-constraints", "dancing-links", "combinatorial-search"]
categories = ["algorithms", "data-structures", "mathematics"]
readme = "README.md"
edition = "2021"
Expand Down
88 changes: 57 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,71 @@
[![docs.rs](https://img.shields.io/docsrs/exact-covers)](https://docs.rs/exact-covers)
[![Build status](https://github.com/hsanzg/exact-covers/actions/workflows/test.yml/badge.svg)](https://github.com/hsanzg/exact-covers/actions/)

Let $I$ be a set of _items_. Given a collection $\mathcal{O}$ of subsets of $I$,
an _exact cover_ of $I$ is a subcollection $\mathcal{O}^\star$ of $\mathcal{O}$
such that each item in $I$ appears in exactly one _option_ in $\mathcal{O}^\star$.
The goal of an exact cover problem is to find one such subset of options
$\mathcal{O}^\star$.

D. E. Knuth proposed a method for solving the exact cover problem in the paper
[_Dancing Links_][dl], whose title refers to a clever yet simple technique
for deleting and restoring the nodes of a doubly linked list.
His backtracking algorithm, called _Algorithm X_, employs this "waltzing"
of links to visit all exact covers of $I$ with options $\mathcal{O}$ in
a recursive, depth-first manner. For further information, see Section 7.2.2.1
of [_The Art of Computer Programming_, Volume 4B, Part 2][taocp4b] (Addison-Wesley,
2022).

A slight modification to this procedure solves the considerably more general
problem in which items fall into one of two categories: _primary_ items,
which _must_ be covered by exactly one option in $\mathcal{O}^\star$, and
_secondary_ items, which _can_ be in at most one option of $\mathcal{O}^\star$.
This crate contains various implementations of Knuth's exact cover solvers
and their data structures in the Rust programming language:
- [`ExactCovers`] finds all exact coverings of $I$ with options in $\mathcal{O}$
under the assumption that there is at least one primary item.
- [`ColoredExactCovers`] will solve the exact cover problem with color constraints.

Also, the [`examples`] directory contains an instructive set of programs that
apply these algorithms to a variety of problems:
<!-- The following section is autogenerated by the cargo-sync-readme utility;
to modify its contents, update the crate documentation in the `src/lib.rs`
file and run the `cargo sync-readme` command. -->

<!-- cargo-sync-readme start -->

This crate provides an implementation of D. E. Knuth's algorithm for solving
the exact cover problem with color controls.

Suppose we're given a collection $\mathcal{O}$ of _options_, each of which is
a set of _items_; the _exact cover_ problem is to find a subcollection
$\mathcal{O}^\star\subseteq\mathcal{O}$ of options such that each item occurs
in exactly one of $\mathcal{O}^\star$'s options. Knuth proposed a method that
achieves this goal in the paper [_Dancing Links_][dl], whose title refers to
a clever yet simple technique for deleting and restoring the nodes of a doubly
linked list. His backtracking scheme, called _Algorithm X_, employs this "waltzing"
of links to visit all exact covers with options $\mathcal{O}$ in a recursive,
depth-first manner. [For further information, see Section 7.2.2.1 of
[_The Art of Computer Programming_ 4B (2022)][taocp4b], Part 2, 65--70.]

A slight modification of Algorithm X solves the considerably more general
problem in which items fall into one of two categories: _primary_ and _secondary_.
Now the task is to find a subcollection $\mathcal{O}^\star\subseteq\mathcal{O}$
of options that cover every primary item _exactly_ once, while covering every
secondary item _at most_ once. The _exact covering with colors_ (XCC) problem
arises if we go further and assign a _color_ to the secondary items of each
option. Then we say two options are _compatible_ if their secondary items
have matching colors, and we define a solution as a collection $\mathcal{O}^\star\subseteq\mathcal{O}$
of mutually compatible options that cover every primary item exactly once.
(In contrast to the uncolored case, a secondary item can occur in more than
one of $\mathcal{O}^\star$'s options provided that their colors are compatible.)

This crate is a library of subroutines for color-controlled covering of
$N_1\geq 0$ primary items and $N_2\geq 0$ secondary items in the Rust
programming language. The following structures are its most important pieces:
- [`Problem`] is a representation of an XCC problem that supports simplification
via the removal of _blocking_ and _forcing_. [For a discussion of these
preprocessing operations, see [Knuth, _The Art of Computer Programming_ 4B (2022)][taocp4b],
Part 2, 108--111.]
- [`Solver`] finds all solutions to an XCC problem.

Also, the [`examples`] directory contains an instructive set of programs
that apply these algorithms to a variety of problems:
- [`langford_pairs.rs`] finds all [Langford pairings] of $2n$ numbers.
- [`polycube_packing.rs`] computes the number of ways to arrange 25 [Y pentacubes]
in a $5\times 5\times 5$ cuboid.
in a $5\times 5\times 5$ cuboid.

[dl]: https://arxiv.org/pdf/cs/0011047.pdf
[taocp4b]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4
[`examples`]: https://github.com/hsanzg/exact-covers/tree/main/examples
[`langford_pairs.rs`]: https://github.com/hsanzg/exact-covers/blob/main/examples/langford_pairs.rs
[Langford pairings]: https://en.wikipedia.org/wiki/Langford_pairing
[`polycube_packing.rs`]: https://github.com/hsanzg/exact-covers/blob/main/examples/polycube_packing.rs
[Y pentacubes]: https://en.wikipedia.org/wiki/Polycube

<!-- cargo-sync-readme end -->

# License
## License

[MIT](LICENSE) &copy; [Hugo Sanz González](https://hgsg.me)

[dl]: https://arxiv.org/pdf/cs/0011047.pdf
[taocp4b]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4
[`ExactCovers`]: https://docs.rs/exact-covers/latest/exact-covers/xc/struct.ExactCovers.html
[`ColoredExactCovers`]: https://docs.rs/exact-covers/latest/exact-covers/xcc/struct.ColoredExactCovers.html
[`Problem`]: https://docs.rs/exact-covers/latest/exact-covers/struct.Problem.html
[`Solver`]: https://docs.rs/exact-covers/latest/exact-covers/struct.Solver.html
[`examples`]: https://github.com/hsanzg/exact-covers/tree/main/examples
[`langford_pairs.rs`]: https://github.com/hsanzg/exact-covers/blob/main/examples/langford_pairs.rs
[Langford pairings]: https://en.wikipedia.org/wiki/Langford_pairing
Expand Down
51 changes: 28 additions & 23 deletions examples/langford_pairs.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
//! Finds all ways to put $2n$ numbers $\\{1,1,2,2,\dots,n,n\\}$ into $2n$ slots
//! so that there are exactly $i$ numbers between the two appearances of $i$,
//! for all $1\leq i\leq n$. This task is known as _Langford's problem_, and
//! its encoding as an exact cover problem is well explained in the book
//! [_The Art of Computer Programming_, Volume 4B, Part 2][taocp4b] (Addison-Wesley,
//! 2022) by D. E. Knuth. His approach can be summarized as follows:
//! The following program finds all ways to put $2n$ numbers $\\{1,1,2,2,\dots,n,n\\}$
//! into $2n$ slots $s_1,\dots,s_{2n}$ so that there are exactly $i$ numbers
//! between the two appearances of $i$, for all $1\leq i\leq n$. This task is
//! known as _Langford's problem_, since it was first described by C. D. Langford
//! [[_The Mathematical Gazette_ 42 (October 1958), 228][mathgaz]]. Its encoding
//! as an exact cover problem is well explained in D. E. Knuth's book
//! [_The Art of Computer Programming_ 4B (2022)][taocp4b], Part 2, page 70.
//! His approach can be summarized as follows:
//!
//! Regard the $n$ values of $i$ and the $2n$ slots as the items to be covered.
//! Then the legal options for permuting the first $n$ integers into a Langford
//! sequence are $`i\\;s_j\\;s_k'$ for $1\leq j<k\leq 2n$, $k=i+j+1$ and $1\leq i
//! \leq n$. In this way the distance between slots $s_j$ and $s_k$ for item $i$
//! is $k-j=i+j+1-j=i+1$, as desired.
//! sequence are $`i\\;s_j\\;s_k'$ for $1\leq i\leq n$, $1\leq j<k\leq 2n$, and
//! $k=i+j+1$. In this way the distance between slots $s_j$ and $s_k$ for item
//! $i$ is $k-j=i+j+1-j=i+1$, as desired.
//!
//! [mathgaz]: https://www.cambridge.org/core/journals/mathematical-gazette/article/abs/problem/557F7BBB739F5B3E0D152C270642B102
//! [taocp4b]: https://www-cs-faculty.stanford.edu/~knuth/taocp.html#vol4

use exact_covers::xc::ExactCovers;
use exact_covers::{Problem, Solver};

// A Langford pair can exist only when $n$ is congruent to 0 or 3 modulo 4.
// This is because the two entries of an odd number must either both go in
// even or in odd positions, while the entries of an even number must fall
// in positions of different parity. There are $\floor{n/2}$ even numbers
// in $\\{1,\dots,n\\}$, so $n-\floor{n/2}=\ceil{n/2}$ positions of each
// parity remain available for the odd numbers. Since these come in pairs
// that occupy positions of the same parity, $\ceil{n/2}$ must be an even
// number. This happens only if $n\equiv 0$ or $n\equiv 3$ (modulo 4).
const N: usize = 8;
/// A Langford pair can exist only when $n$ is congruent to 0 or 3 modulo 4.
/// This is because the two entries of an odd number must either both go in
/// even or in odd positions, while the entries of an even number must fall
/// in positions of different parity. There are $\lfloor n/2\rfloor$ even
/// numbers in $\\{1,\dots,n\\}$, so $n-\lfloor n/2\rfloor=\lceil n/2\rceil$
/// positions of each parity remain available for the odd numbers. Since these
/// come in pairs that occupy positions of the same parity, $\lceil n/2\rceil$
/// must be an even number. This happens only if $n\equiv 0$ or $n\equiv 3$
/// (modulo 4).
const N: usize = 15;

#[derive(Eq, PartialEq, Copy, Clone, Ord, PartialOrd)]
enum Item {
Expand All @@ -32,11 +36,11 @@ enum Item {
}

fn main() {
let numbers = (1..=N).map(Item::Number);
/*let numbers = (1..=N).map(Item::Number);
let slots = (1..=2 * N).map(Item::Slot);
let items: Vec<_> = numbers.chain(slots).collect();
let mut solver = ExactCovers::new(&items, &[]);
let mut problem = Problem::new(&items, &[]);
for i in 1..=N {
// Optimization: half of the Langford pairs for a given value of $n$
// are the reverses of the others. Reduce the search space by placing
Expand All @@ -45,10 +49,11 @@ fn main() {
for j in first_slot_range {
let k = i + j + 1;
let option = [&Item::Number(i), &Item::Slot(j), &Item::Slot(k)];
solver.add_option(option);
problem.add_option(option);
}
}
let mut solver = Solver::solve(problem);
let mut options = Vec::new();
solver.solve(|mut solution| {
assert_eq!(solution.option_count(), N);
Expand All @@ -70,5 +75,5 @@ fn main() {
println!("{:?}", placement);
placement.reverse();
println!("{:?}", placement);
})
});*/
}
11 changes: 0 additions & 11 deletions examples/polycube_packing.rs

This file was deleted.

71 changes: 42 additions & 29 deletions src/indices.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,65 @@
use std::fmt::Debug;
use std::num::NonZeroUsize;

/// The position of an item node in a sequential table.
///
/// See the `items` arena in the [`ExactCovers`] struct for an example of
/// See the `items` arena in the [`Problem`] structure for an example of
/// this construction.
///
/// [`ExactCovers`]: `crate::xc::ExactCovers`
/// [`Problem`]: `crate::Problem`
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[repr(transparent)]
pub(crate) struct ItemIndex(usize);

impl ItemIndex {
/// Creates a new index.
#[must_use]
pub const fn new(ix: usize) -> Self {
Self(ix)
}

/// Returns the index value as a primitive type.
#[must_use]
pub const fn get(self) -> usize {
self.0
}

/// Returns the position of the previous item in the table.
///
/// The result is meaningful only if `self` is positive.
#[must_use]
pub fn decrement(self) -> Self {
Self(self.0 - 1)
}

/// Returns the position of the next record in the table, if any.
/// Returns the position of the next item in the table, if any.
#[must_use]
pub fn increment(self) -> Self {
Self(self.0 + 1)
}
}

/// The position of a node in a sequential table, whose first node cannot
/// be referenced. See the `nodes` arena in the [`ExactCovers`] struct for
/// The position of a record in a sequential table, whose first element cannot
/// be referenced. See the `records` arena in the [`Problem`] structure for
/// an example of this construction.
///
/// The restriction to positive index values may seem awkward, but it is
/// the only way to exploit the memory layout advantages of [`NonZeroUsize`]
/// while letting [`NodeIndex::get`] be a simple getter that performs no
/// conversion through e.g. bitwise complementation. This additional
/// while letting [`RecordIndex::get`] be a simple getter that performs
/// no conversion through e.g. bitwise complementation. This additional
/// computation appears in roughly 3% of `perf` samples when running
/// various benchmarks problems. Fortunately, exact cover solvers rarely
/// if ever need to access the first node by index, because it is a spacer
/// (see [`ExactCovers`] for details).
/// various benchmark problems. Fortunately, [`Solver`] rarely needs
/// to access the first record by index, because it is a [spacer].
///
/// [`ExactCovers`]: `crate::xc::ExactCovers`
/// [`Problem`]: `crate::Problem`
/// [`Solver`]: `crate::Solver`
/// [spacer]: `crate::problem::Record::Spacer`
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[repr(transparent)]
pub(crate) struct NodeIndex(NonZeroUsize);
pub(crate) struct RecordIndex(NonZeroUsize);

impl NodeIndex {
impl RecordIndex {
/// Creates a new index.
#[must_use]
pub const fn new(ix: usize) -> Self {
// Workaround for `Option::expect` not being `const fn` in stable Rust.
Self(if let Some(ix) = NonZeroUsize::new(ix) {
Expand All @@ -65,24 +70,29 @@ impl NodeIndex {
}

/// Returns the index value as a primitive type.
#[must_use]
pub const fn get(self) -> usize {
self.0.get()
}

/// Returns the position of the previous node in the table, or `None`
/// if this index refers to the second record in the table (that is,
/// whenever `self.get() == 1`).
/// Returns the position of the previous record in the table, or [`None`]
/// if the result would refer to the first [spacer] (that is, whenever
/// `self.get() == 1`).
///
/// [spacer]: `crate::problem::Record::Spacer`
#[must_use]
pub fn decrement(self) -> Option<Self> {
NonZeroUsize::new(self.0.get() - 1).map(Self)
}

/// Returns the position of the next node in the table, if any.
/// Returns the position of the next record in the table, if any.
#[must_use]
pub fn increment(self) -> Self {
// Workaround for `Option::expect` not being `const fn` in stable Rust.
Self(if let Some(ix) = self.0.checked_add(1) {
ix
} else {
panic!("overflow in NodeIndex::increment")
panic!("overflow in RecordIndex::increment")
})
}
}
Expand All @@ -97,25 +107,28 @@ mod tests {
assert_eq!(ItemIndex::new(123).get(), 123);
assert_eq!(ItemIndex::new(456789).get(), 456789);

assert_eq!(NodeIndex::new(1).get(), 1);
assert_eq!(NodeIndex::new(65).get(), 65);
assert_eq!(NodeIndex::new(87935).get(), 87935);
assert_eq!(RecordIndex::new(1).get(), 1);
assert_eq!(RecordIndex::new(65).get(), 65);
assert_eq!(RecordIndex::new(87935).get(), 87935);
}

#[test]
#[should_panic]
fn out_of_bounds_node_index() {
NodeIndex::new(0);
RecordIndex::new(0);
}

#[test]
fn index_decrement() {
assert_eq!(ItemIndex::new(1).decrement(), ItemIndex::new(0));
assert_eq!(ItemIndex::new(15).decrement(), ItemIndex::new(14));

assert_eq!(NodeIndex::new(1).decrement(), None);
assert_eq!(NodeIndex::new(2).decrement(), Some(NodeIndex::new(1)));
assert_eq!(NodeIndex::new(565).decrement(), Some(NodeIndex::new(564)));
assert!(RecordIndex::new(1).decrement().is_none());
assert_eq!(RecordIndex::new(2).decrement(), Some(RecordIndex::new(1)));
assert_eq!(
RecordIndex::new(565).decrement(),
Some(RecordIndex::new(564))
);
}

#[test]
Expand All @@ -124,8 +137,8 @@ mod tests {
assert_eq!(ItemIndex::new(1).increment(), ItemIndex::new(2));
assert_eq!(ItemIndex::new(133).increment(), ItemIndex::new(134));

assert_eq!(NodeIndex::new(1).increment(), NodeIndex::new(2));
assert_eq!(NodeIndex::new(2).increment(), NodeIndex::new(3));
assert_eq!(NodeIndex::new(234).increment(), NodeIndex::new(235));
assert_eq!(RecordIndex::new(1).increment(), RecordIndex::new(2));
assert_eq!(RecordIndex::new(2).increment(), RecordIndex::new(3));
assert_eq!(RecordIndex::new(234).increment(), RecordIndex::new(235));
}
}

0 comments on commit 591888d

Please sign in to comment.