type Cell = (usize, usize);
const HAMILTONIAN_BUDGET: u64 = 400_000;
const SOLVE_BUDGET: u64 = 50_000;
const MAX_ATTEMPTS: u32 = 500;
const UNIQUENESS_ATTEMPTS: u32 = 30;
const CUT_ATTEMPTS: u32 = 40;
fn to_idx(cell: Cell, width: usize) -> usize {
cell.1 * width + cell.0
}
fn neighbors(cell: Cell, width: usize, height: usize) -> [Option<Cell>; 4] {
let (x, y) = cell;
[
(x > 0).then(|| (x - 1, y)),
(x + 1 < width).then_some((x + 1, y)),
(y > 0).then(|| (x, y - 1)),
(y + 1 < height).then_some((x, y + 1)),
]
}
pub(crate) fn is_adjacent(a: Cell, b: Cell) -> bool {
let dx = a.0.abs_diff(b.0);
let dy = a.1.abs_diff(b.1);
(dx == 1 && dy == 0) || (dx == 0 && dy == 1)
}
fn random_hamiltonian_path(
width: usize,
height: usize,
rng: &mut fastrand::Rng,
) -> Option<Vec<Cell>> {
let n = width * height;
let start = (rng.usize(..width), rng.usize(..height));
let mut visited = vec![false; n];
visited[to_idx(start, width)] = true;
let mut path = vec![start];
let mut frames: Vec<Vec<Cell>> = vec![ranked_neighbors(start, width, height, &visited, rng)];
let mut budget = HAMILTONIAN_BUDGET;
while !frames.is_empty() {
if path.len() == n {
return Some(path);
}
if budget == 0 {
return None;
}
budget -= 1;
let frame = frames.last_mut().expect("loop condition ensures non-empty");
let mut advanced = false;
while let Some(candidate) = frame.pop() {
let ci = to_idx(candidate, width);
if !visited[ci] {
visited[ci] = true;
path.push(candidate);
frames.push(ranked_neighbors(candidate, width, height, &visited, rng));
advanced = true;
break;
}
}
if !advanced {
frames.pop();
let removed = path.pop().expect("frames/path stay in lockstep");
visited[to_idx(removed, width)] = false;
}
}
None
}
fn ranked_neighbors(
cell: Cell,
width: usize,
height: usize,
visited: &[bool],
rng: &mut fastrand::Rng,
) -> Vec<Cell> {
let mut candidates: Vec<Cell> = neighbors(cell, width, height)
.into_iter()
.flatten()
.filter(|&c| !visited[to_idx(c, width)])
.collect();
rng.shuffle(&mut candidates);
let degree = |c: Cell| -> usize {
neighbors(c, width, height)
.into_iter()
.flatten()
.filter(|&n| !visited[to_idx(n, width)])
.count()
};
candidates.sort_by_key(|&c| std::cmp::Reverse(degree(c)));
candidates
}
fn cut_into_segments(path: &[Cell], pair_count: usize, rng: &mut fastrand::Rng) -> Vec<Vec<Cell>> {
let mut last = None;
for _ in 0..CUT_ATTEMPTS {
let segments = cut_once(path, pair_count, rng);
let any_adjacent = segments
.iter()
.any(|s| is_adjacent(s[0], *s.last().expect("segment has >= 2 cells")));
if !any_adjacent {
return segments;
}
last = Some(segments);
}
last.expect("CUT_ATTEMPTS > 0, so at least one attempt was made")
}
fn cut_once(path: &[Cell], pair_count: usize, rng: &mut fastrand::Rng) -> Vec<Vec<Cell>> {
debug_assert!(path.len() >= pair_count * 2);
let target_len = path.len() / pair_count;
let jitter = (target_len / 4).max(1);
let mut cuts: Vec<usize> = Vec::with_capacity(pair_count.saturating_sub(1));
let mut boundary = 0usize;
for remaining_segments in (1..pair_count).rev() {
let min = boundary + 2;
let max = path.len() - remaining_segments * 2;
let target = (boundary + target_len).clamp(min, max);
let lo = target.saturating_sub(jitter).max(min);
let hi = (target + jitter).min(max);
let cut = if hi > lo {
rng.usize(lo..=hi)
} else {
min.min(max)
};
cuts.push(cut);
boundary = cut;
}
let mut segments = Vec::with_capacity(pair_count);
let mut start = 0;
for &cut in &cuts {
segments.push(path[start..cut].to_vec());
start = cut;
}
segments.push(path[start..].to_vec());
segments
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum SolveOutcome {
Count(u32),
BudgetExceeded,
}
struct Solver {
width: usize,
height: usize,
owner: Vec<Option<usize>>,
heads: Vec<Cell>,
targets: Vec<Cell>,
done: Vec<bool>,
filled: usize,
}
impl Solver {
fn new(width: usize, height: usize, endpoints: &[(Cell, Cell)]) -> Self {
let mut owner = vec![None; width * height];
let mut heads = Vec::with_capacity(endpoints.len());
let mut targets = Vec::with_capacity(endpoints.len());
for (n, &(a, b)) in endpoints.iter().enumerate() {
owner[to_idx(a, width)] = Some(n);
heads.push(a);
targets.push(b);
}
Self {
width,
height,
owner,
heads,
targets,
done: vec![false; endpoints.len()],
filled: endpoints.len(),
}
}
fn legal_moves(&self, number: usize) -> Vec<Cell> {
neighbors(self.heads[number], self.width, self.height)
.into_iter()
.flatten()
.filter(|&c| {
let idx = to_idx(c, self.width);
match self.owner[idx] {
None => true,
Some(other) => other == number && c == self.targets[number],
}
})
.collect()
}
fn apply(&mut self, number: usize, cell: Cell) {
self.owner[to_idx(cell, self.width)] = Some(number);
self.heads[number] = cell;
self.filled += 1;
if cell == self.targets[number] {
self.done[number] = true;
}
}
fn undo(&mut self, number: usize, cell: Cell, prev_head: Cell) {
self.owner[to_idx(cell, self.width)] = None;
self.heads[number] = prev_head;
self.filled -= 1;
self.done[number] = false;
}
fn is_feasible(&self) -> bool {
for y in 0..self.height {
for x in 0..self.width {
let cell = (x, y);
if self.owner[to_idx(cell, self.width)].is_some() {
continue;
}
let usable = neighbors(cell, self.width, self.height)
.into_iter()
.flatten()
.filter(|&n| {
let ni = to_idx(n, self.width);
self.owner[ni].is_none()
|| self
.heads
.iter()
.zip(&self.done)
.any(|(&h, &done)| !done && h == n)
})
.count();
let is_pending_target = self
.targets
.iter()
.zip(&self.done)
.any(|(&t, &done)| !done && t == cell);
let required = if is_pending_target { 1 } else { 2 };
if usable < required {
return false;
}
}
}
true
}
fn count_solutions(&mut self, limit: u32, budget: &mut u64) -> SolveOutcome {
let mut count = 0u32;
if self.dfs(limit, budget, &mut count) {
SolveOutcome::Count(count)
} else {
SolveOutcome::BudgetExceeded
}
}
fn dfs(&mut self, limit: u32, budget: &mut u64, count: &mut u32) -> bool {
if *budget == 0 {
return false;
}
*budget -= 1;
if self.filled == self.width * self.height {
if self.done.iter().all(|&d| d) {
*count += 1;
}
return *count < limit;
}
let mut chosen: Option<(usize, Vec<Cell>)> = None;
for n in 0..self.heads.len() {
if self.done[n] {
continue;
}
let moves = self.legal_moves(n);
if moves.is_empty() {
return true; }
if chosen.as_ref().map_or(true, |(_, m)| moves.len() < m.len()) {
chosen = Some((n, moves));
}
}
let Some((number, moves)) = chosen else {
return true;
};
for mv in moves {
let prev_head = self.heads[number];
self.apply(number, mv);
let feasible = self.is_feasible();
let keep_going = if feasible {
self.dfs(limit, budget, count)
} else {
true };
self.undo(number, mv, prev_head);
if !keep_going || *count >= limit {
return keep_going;
}
}
true
}
}
pub(crate) fn generate(
width: usize,
height: usize,
pair_count: usize,
seed: u64,
) -> Vec<(Cell, Cell)> {
assert!(
width * height >= pair_count * 2,
"not enough cells for {pair_count} pairs on a {width}x{height} board"
);
let mut rng = fastrand::Rng::with_seed(seed);
let mut fallback: Option<Vec<(Cell, Cell)>> = None;
for attempt in 0..MAX_ATTEMPTS {
let Some(path) = random_hamiltonian_path(width, height, &mut rng) else {
continue;
};
let segments = cut_into_segments(&path, pair_count, &mut rng);
let endpoints: Vec<(Cell, Cell)> = segments
.iter()
.map(|s| (s[0], *s.last().expect("segment has >= 2 cells")))
.collect();
if fallback.is_none() {
fallback = Some(endpoints.clone());
}
if attempt >= UNIQUENESS_ATTEMPTS {
break;
}
let mut budget = SOLVE_BUDGET;
let mut solver = Solver::new(width, height, &endpoints);
if let SolveOutcome::Count(1) = solver.count_solutions(2, &mut budget) {
return endpoints;
}
}
fallback.expect(
"random_hamiltonian_path succeeds within a handful of attempts on any reasonable board",
)
}
#[cfg(test)]
mod tests {
use super::*;
fn count_solutions_for(
width: usize,
height: usize,
endpoints: &[(Cell, Cell)],
limit: u32,
) -> SolveOutcome {
let mut solver = Solver::new(width, height, endpoints);
let mut budget = SOLVE_BUDGET;
solver.count_solutions(limit, &mut budget)
}
#[test]
fn is_adjacent_only_true_for_orthogonal_neighbors() {
assert!(is_adjacent((1, 1), (1, 2)));
assert!(is_adjacent((1, 1), (2, 1)));
assert!(!is_adjacent((1, 1), (2, 2)));
assert!(!is_adjacent((1, 1), (1, 1)));
}
#[test]
fn generate_produces_the_requested_shape() {
for seed in 0..8 {
let endpoints = generate(4, 4, 4, seed);
assert_eq!(endpoints.len(), 4);
let mut seen = std::collections::HashSet::new();
for &(a, b) in &endpoints {
assert_ne!(a, b);
assert!(seen.insert(a));
assert!(seen.insert(b));
}
}
}
#[test]
fn generate_succeeds_on_doneward_s_actual_difficulty_presets() {
for (width, height, pairs) in [(5, 5, 4), (7, 7, 6), (9, 9, 8)] {
for seed in 0..3 {
let endpoints = generate(width, height, pairs, seed);
assert_eq!(endpoints.len(), pairs);
}
}
}
#[test]
fn generated_pairs_are_never_trivially_adjacent() {
for (width, height, pairs) in [(5, 5, 4), (7, 7, 6), (9, 9, 8)] {
for seed in 0..10 {
let endpoints = generate(width, height, pairs, seed);
for &(a, b) in &endpoints {
assert!(
!is_adjacent(a, b),
"pair {a:?}-{b:?} is directly adjacent on a {width}x{height}/{pairs} \
board (seed {seed})"
);
}
}
}
}
#[test]
fn generate_is_verifiably_unique() {
let endpoints = generate(4, 4, 3, 42);
assert_eq!(
count_solutions_for(4, 4, &endpoints, 2),
SolveOutcome::Count(1)
);
}
#[test]
fn a_two_by_two_board_with_two_diagonal_pairs_is_unique() {
let endpoints = [((0, 0), (1, 0)), ((0, 1), (1, 1))];
assert_eq!(
count_solutions_for(2, 2, &endpoints, 2),
SolveOutcome::Count(1)
);
}
#[test]
fn a_two_by_two_board_with_a_crossing_pair_layout_is_not_unique_or_impossible() {
let endpoints = [((0, 0), (1, 1)), ((1, 0), (0, 1))];
match count_solutions_for(2, 2, &endpoints, 4) {
SolveOutcome::Count(_) => {}
SolveOutcome::BudgetExceeded => panic!("solver should finish instantly on a 2x2 board"),
}
}
}