Skip to content

Commit 79476df

Browse files
committed
dominoes: replace example with sound solution
It was discovered that the previous example solution doesn't perform any backtracking and only passes the relevant test accidentally: #2127 (comment)
1 parent 198d36d commit 79476df

File tree

1 file changed

+64
-106
lines changed

1 file changed

+64
-106
lines changed
Lines changed: 64 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,83 @@
1-
pub type Domino = (u8, u8);
1+
//! This exercise is about finding a [Eulerian path]. The "dots" are the
2+
//! vertices of the graph, while the dominoes are the edges.
3+
//!
4+
//! [Eulerian path]: https://en.wikipedia.org/wiki/Eulerian_path
25
3-
/// A table keeping track of available dominoes.
4-
///
5-
/// Effectively a 6x6 matrix. Each position denotes whether a domino is available with that column
6-
/// dots and row dots. Positions are mirrored ((3,4) == (4,3)), except for positions with equal row
7-
/// and column numbers.
8-
struct AvailabilityTable {
9-
m: Vec<u8>,
10-
}
6+
type Domino = (u8, u8);
117

12-
impl AvailabilityTable {
13-
fn new() -> AvailabilityTable {
14-
AvailabilityTable {
15-
m: std::iter::repeat_n(0, 6 * 6).collect(),
16-
}
8+
pub fn chain(input: &[Domino]) -> Option<Vec<Domino>> {
9+
let mut bag = DominoBag::default();
10+
for domino in input.iter().copied() {
11+
bag.insert(domino);
1712
}
1813

19-
fn get(&self, x: u8, y: u8) -> u8 {
20-
self.m[((x - 1) * 6 + (y - 1)) as usize]
21-
}
14+
let mut chain = Vec::with_capacity(input.len());
15+
let mut tail = vec![]; // used for temporary storage
2216

23-
fn set(&mut self, x: u8, y: u8, v: u8) {
24-
let m = &mut self.m[..];
25-
m[((x - 1) * 6 + (y - 1)) as usize] = v;
26-
}
17+
// start with any domino. (default will cause empty chain to be returned)
18+
let (mut first_dots, mut current_dots) = (0..7)
19+
.find_map(|i| bag.take_neighbor(i))
20+
.inspect(|&d| chain.push(d))
21+
.unwrap_or_default();
2722

28-
fn add(&mut self, x: u8, y: u8) {
29-
if x == y {
30-
let n = self.get(x, y);
31-
self.set(x, y, n + 1) // Along the diagonal
32-
} else {
33-
let m = self.get(x, y);
34-
self.set(x, y, m + 1);
35-
let n = self.get(y, x);
36-
self.set(y, x, n + 1);
23+
loop {
24+
while let Some(next_domoino) = bag.take_neighbor(current_dots) {
25+
chain.push(next_domoino);
26+
current_dots = next_domoino.1;
3727
}
38-
}
39-
40-
fn remove(&mut self, x: u8, y: u8) {
41-
if self.get(x, y) > 0 {
42-
if x == y {
43-
let n = self.get(x, y);
44-
self.set(x, y, n - 1) // Along the diagonal
45-
} else {
46-
let m = self.get(x, y);
47-
self.set(x, y, m - 1);
48-
let n = self.get(y, x);
49-
self.set(y, x, n - 1);
50-
}
51-
} else {
52-
// For this toy code hard explicit fail is best
53-
panic!("remove for 0 stones: ({x:?}, {y:?})")
28+
if current_dots != first_dots {
29+
return None; // unbalanced
5430
}
55-
}
31+
// reintegrate second chain half from previous loop iteration
32+
chain.append(&mut tail);
5633

57-
fn pop_first(&mut self, x: u8) -> Option<u8> {
58-
if self.get(x, x) > 0 {
59-
self.remove(x, x);
60-
return Some(x);
34+
if bag.is_empty() {
35+
return Some(chain);
6136
}
37+
// We have found a path that ends where it started, but not all dominoes
38+
// are used up. We must find a location in the current chain where we
39+
// could've taken a different path.
40+
let (fork_point, next_domino) = chain
41+
.iter()
42+
.enumerate()
43+
.find_map(|(i, &(x, _))| bag.take_neighbor(x).map(|d| (i, d)))?;
6244

63-
for y in 1..7 {
64-
if self.get(x, y) > 0 {
65-
self.remove(x, y);
66-
return Some(y);
67-
}
68-
}
69-
None
45+
// put aside second half of first chain
46+
tail.extend(chain.drain(fork_point..));
47+
48+
chain.push(next_domino);
49+
// Treat the domino after the fork point as the first domino, to search
50+
// for a path that ends up back at the fork point.
51+
(first_dots, current_dots) = next_domino;
7052
}
7153
}
7254

73-
pub fn chain(dominoes: &[Domino]) -> Option<Vec<Domino>> {
74-
match dominoes.len() {
75-
0 => Some(vec![]),
76-
1 => {
77-
if dominoes[0].0 == dominoes[0].1 {
78-
Some(vec![dominoes[0]])
79-
} else {
80-
None
81-
}
82-
}
83-
_ => {
84-
// First check if the total number of each amount of dots is even, if not it's not
85-
// possible to complete a cycle. This follows from that it's an Eulerian path.
86-
let mut v: Vec<u8> = vec![0, 0, 0, 0, 0, 0];
87-
// Keep the mutable borrow in a small scope here to allow v.iter().
88-
{
89-
let vs = &mut v[..];
90-
for dom in dominoes.iter() {
91-
vs[dom.0 as usize - 1] += 1;
92-
vs[dom.1 as usize - 1] += 1;
93-
}
94-
}
95-
for n in v.iter() {
96-
if n % 2 != 0 {
97-
return None;
98-
}
99-
}
100-
let chain = chain_worker(dominoes);
101-
if chain.len() == dominoes.len() {
102-
Some(chain)
103-
} else {
104-
None
105-
}
106-
}
55+
/// The domino bag stores all "untraversed edges" of the graph, using an
56+
/// adjacency matrix.
57+
#[derive(Default)]
58+
struct DominoBag([[u8; 7]; 7]);
59+
60+
impl DominoBag {
61+
fn is_empty(&self) -> bool {
62+
self.0.iter().flatten().all(|d| *d == 0)
10763
}
108-
}
10964

110-
fn chain_worker(dominoes: &[Domino]) -> Vec<Domino> {
111-
let mut doms = dominoes.to_vec();
112-
let first = doms.pop().unwrap();
113-
let mut t = AvailabilityTable::new();
114-
for dom in doms.iter() {
115-
t.add(dom.0, dom.1)
65+
fn insert(&mut self, d: Domino) {
66+
let (i, j) = (d.0 as usize, d.1 as usize);
67+
self.0[i][j] += 1;
68+
self.0[j][i] += 1;
11669
}
117-
let mut v: Vec<Domino> = Vec::new();
118-
v.push(first);
119-
let mut n = first.1; // Number to connect to
120-
while let Some(m) = t.pop_first(n) {
121-
v.push((n, m));
122-
n = m;
70+
71+
/// Takes a domino connecting `i` to any neighbor.
72+
fn take_neighbor(&mut self, i: u8) -> Option<Domino> {
73+
(0..7).map(|j| (i, j)).find(|d| {
74+
let (i, j) = (d.0 as usize, d.1 as usize);
75+
if self.0[i][j] == 0 {
76+
return false;
77+
};
78+
self.0[i][j] -= 1;
79+
self.0[j][i] -= 1;
80+
true
81+
})
12382
}
124-
v
12583
}

0 commit comments

Comments
 (0)