type Cell = (usize, usize);
type CutScore = (f64, f64);
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 = 500;
const PAIR_DISTANCE_FLOOR: usize = 3;
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 manhattan(a: Cell, b: Cell) -> usize {
a.0.abs_diff(b.0) + a.1.abs_diff(b.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 random_cut_points(path_len: usize, pair_count: usize, rng: &mut fastrand::Rng) -> Vec<usize> {
debug_assert!(path_len >= pair_count * 2);
loop {
let mut cuts: Vec<usize> = (0..pair_count - 1)
.map(|_| rng.usize(1..path_len))
.collect();
cuts.sort_unstable();
cuts.dedup();
if cuts.len() != pair_count - 1 {
continue; }
let mut boundary = 0;
let mut ok = true;
for &cut in &cuts {
if cut - boundary < 2 {
ok = false;
break;
}
boundary = cut;
}
if ok && path_len - boundary >= 2 {
return cuts;
}
}
}
fn score_cuts(segments: &[Vec<Cell>]) -> CutScore {
let fractions: Vec<f64> = segments
.iter()
.map(|s| {
let first = s[0];
let last = *s.last().expect("segment has >= 2 cells");
let dist = manhattan(first, last);
if dist < PAIR_DISTANCE_FLOOR {
return 0.0;
}
dist as f64 / (s.len() - 1) as f64
})
.collect();
if fractions.contains(&0.0) {
return (0.0, 0.0);
}
let min = fractions.iter().copied().fold(f64::INFINITY, f64::min);
let mean = fractions.iter().sum::<f64>() / fractions.len() as f64;
(min, mean)
}
fn cut_into_segments(path: &[Cell], pair_count: usize, rng: &mut fastrand::Rng) -> Vec<Vec<Cell>> {
let mut best: Option<(Vec<Vec<Cell>>, CutScore)> = None;
for _ in 0..CUT_ATTEMPTS {
let cuts = random_cut_points(path.len(), pair_count, rng);
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());
let score = score_cuts(&segments);
let is_better = match &best {
Some((_, best_score)) => score > *best_score,
None => true,
};
if is_better {
best = Some((segments, score));
}
}
best.expect("CUT_ATTEMPTS > 0, so at least one attempt was made")
.0
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum SolveOutcome {
Count(u32),
BudgetExceeded,
}
struct Solver {
width: usize,
height: usize,
owner: Vec<Option<usize>>,
blocked: Vec<bool>,
heads: Vec<Cell>,
targets: Vec<Cell>,
target_of: Vec<Option<usize>>,
done: Vec<bool>,
filled: usize,
usable_cells: usize,
}
impl Solver {
fn new(
width: usize,
height: usize,
endpoints: &[(Cell, Cell)],
blocked_cells: &[Cell],
) -> Self {
let mut owner = vec![None; width * height];
let mut blocked = vec![false; width * height];
for &c in blocked_cells {
blocked[to_idx(c, width)] = true;
}
let mut target_of = 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);
target_of[to_idx(b, width)] = Some(n);
heads.push(a);
targets.push(b);
}
let usable_cells = width * height - blocked.iter().filter(|&&b| b).count();
Self {
width,
height,
owner,
blocked,
heads,
targets,
target_of,
done: vec![false; endpoints.len()],
filled: endpoints.len(),
usable_cells,
}
}
fn enterable(&self, number: usize, cell: Cell) -> bool {
let idx = to_idx(cell, self.width);
if self.blocked[idx] || self.owner[idx].is_some() {
return false;
}
!matches!(self.target_of[idx], Some(other) if other != number && !self.done[other])
}
fn legal_moves(&self, number: usize) -> Vec<Cell> {
neighbors(self.heads[number], self.width, self.height)
.into_iter()
.flatten()
.filter(|&c| self.enterable(number, c))
.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);
let idx = to_idx(cell, self.width);
if self.blocked[idx] || self.owner[idx].is_some() {
continue;
}
let usable = neighbors(cell, self.width, self.height)
.into_iter()
.flatten()
.filter(|&n| {
let ni = to_idx(n, self.width);
!self.blocked[ni]
&& (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.usable_cells {
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)>, CutScore)> = 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 score = score_cuts(&segments);
let endpoints: Vec<(Cell, Cell)> = segments
.iter()
.map(|s| (s[0], *s.last().expect("segment has >= 2 cells")))
.collect();
let is_better = match &fallback {
Some((_, best)) => score > *best,
None => true,
};
if is_better {
fallback = Some((endpoints.clone(), score));
}
if score.0 == 0.0 {
continue;
}
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",
)
.0
}
#[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);
let mut solver = Solver::new(4, 4, &endpoints, &[]);
let mut budget = SOLVE_BUDGET;
assert_eq!(
solver.count_solutions(2, &mut budget),
SolveOutcome::Count(1)
);
}
#[test]
fn cut_into_segments_partitions_the_whole_path() {
let mut rng = fastrand::Rng::with_seed(7);
for (width, height, pairs) in [(5, 5, 4), (7, 7, 6), (9, 9, 8)] {
let mut path = None;
for _ in 0..MAX_ATTEMPTS {
if let Some(p) = random_hamiltonian_path(width, height, &mut rng) {
path = Some(p);
break;
}
}
let path = path.expect("small boards find a Hamiltonian path within a few attempts");
let segments = cut_into_segments(&path, pairs, &mut rng);
assert_eq!(segments.len(), pairs);
let mut rebuilt = Vec::with_capacity(path.len());
for segment in &segments {
assert!(
segment.len() >= 2,
"segment {segment:?} has fewer than 2 cells"
);
rebuilt.extend(segment.iter().copied());
}
assert_eq!(
rebuilt, path,
"segments must concatenate back to the original path in order"
);
}
}
#[test]
fn cut_into_segments_finds_well_spread_endpoints_across_seeds() {
for (width, height, pairs) in [(5, 5, 4), (7, 7, 6), (9, 9, 8)] {
for seed in 0..20 {
let mut rng = fastrand::Rng::with_seed(seed);
let mut segments = None;
for _ in 0..MAX_ATTEMPTS {
let Some(path) = random_hamiltonian_path(width, height, &mut rng) else {
continue;
};
let candidate = cut_into_segments(&path, pairs, &mut rng);
if score_cuts(&candidate).0 > 0.0 {
segments = Some(candidate);
break;
}
}
let segments = segments
.expect("a non-degenerate cut should be found within MAX_ATTEMPTS fresh paths");
let (min_fraction, _mean) = score_cuts(&segments);
assert!(
min_fraction > 0.0,
"{width}x{height}/{pairs} seed {seed}: every segment should clear \
PAIR_DISTANCE_FLOOR, got min_fraction {min_fraction}"
);
for segment in &segments {
let first = segment[0];
let last = *segment.last().unwrap();
let dist = manhattan(first, last);
assert!(
dist >= PAIR_DISTANCE_FLOOR,
"{width}x{height}/{pairs} seed {seed}: segment {first:?}-{last:?} only \
{dist} apart, expected at least {PAIR_DISTANCE_FLOOR}"
);
}
}
}
}
#[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"),
}
}
#[test]
fn blocked_cells_are_never_entered() {
let endpoints = [((0, 0), (2, 0))];
let mut solver = Solver::new(3, 1, &endpoints, &[(1, 0)]);
let mut budget = SOLVE_BUDGET;
assert_eq!(
solver.count_solutions(2, &mut budget),
SolveOutcome::Count(0)
);
}
}