use crate::error::{self, Result};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DimerCovering {
pub dimers: Vec<(usize, usize)>,
pub partner: Vec<Option<usize>>,
pub monomers: Vec<usize>,
}
impl DimerCovering {
pub fn new(num_sites: usize, dimers: Vec<(usize, usize)>) -> Result<Self> {
let mut partner: Vec<Option<usize>> = vec![None; num_sites];
let mut canon_dimers: Vec<(usize, usize)> = Vec::with_capacity(dimers.len());
for (i, j) in dimers {
if i >= num_sites || j >= num_sites {
return Err(error::invalid_param(
"dimers",
"dimer references a site index out of range",
));
}
if i == j {
return Err(error::invalid_param(
"dimers",
"a dimer cannot connect a site to itself",
));
}
if partner[i].is_some() || partner[j].is_some() {
return Err(error::invalid_param(
"dimers",
"a site cannot belong to more than one dimer in a covering",
));
}
partner[i] = Some(j);
partner[j] = Some(i);
canon_dimers.push((i.min(j), i.max(j)));
}
canon_dimers.sort_unstable();
let monomers: Vec<usize> = (0..num_sites).filter(|&i| partner[i].is_none()).collect();
Ok(Self {
dimers: canon_dimers,
partner,
monomers,
})
}
pub fn num_sites(&self) -> usize {
self.partner.len()
}
pub fn is_paired(&self, i: usize, j: usize) -> bool {
self.partner.get(i).copied().flatten() == Some(j)
}
pub fn partner_of(&self, i: usize) -> Option<usize> {
self.partner.get(i).copied().flatten()
}
pub fn enumerate_perfect_matchings(
num_sites: usize,
bonds: &[(usize, usize)],
) -> Result<Vec<DimerCovering>> {
if num_sites == 0 || num_sites % 2 != 0 {
return Err(error::invalid_param(
"num_sites",
"a perfect dimer matching requires a positive, even number of sites",
));
}
let adjacency = build_adjacency(num_sites, bonds)?;
let mut used = vec![false; num_sites];
let mut current = Vec::with_capacity(num_sites / 2);
let mut results = Vec::new();
backtrack_matching(&adjacency, &mut used, &mut current, &mut results, &[]);
results
.into_iter()
.map(|dimers| DimerCovering::new(num_sites, dimers))
.collect()
}
pub fn enumerate_monomer_dimer_matchings(
num_sites: usize,
bonds: &[(usize, usize)],
monomer_a: usize,
monomer_b: usize,
) -> Result<Vec<DimerCovering>> {
if monomer_a >= num_sites || monomer_b >= num_sites {
return Err(error::invalid_param(
"monomer_a/monomer_b",
"monomer site index out of range",
));
}
if monomer_a == monomer_b {
return Err(error::invalid_param(
"monomer_a/monomer_b",
"the two monomer sites must be distinct",
));
}
if (num_sites - 2) % 2 != 0 {
return Err(error::invalid_param(
"num_sites",
"removing the two monomer sites must leave an even number of sites to match",
));
}
let adjacency = build_adjacency(num_sites, bonds)?;
let excluded = [monomer_a, monomer_b];
let mut used = vec![false; num_sites];
used[monomer_a] = true;
used[monomer_b] = true;
let mut current = Vec::with_capacity((num_sites - 2) / 2);
let mut results = Vec::new();
backtrack_matching(&adjacency, &mut used, &mut current, &mut results, &excluded);
results
.into_iter()
.map(|dimers| DimerCovering::new(num_sites, dimers))
.collect()
}
}
fn backtrack_matching(
adjacency: &[Vec<usize>],
used: &mut [bool],
current: &mut Vec<(usize, usize)>,
results: &mut Vec<Vec<(usize, usize)>>,
excluded: &[usize],
) {
let num_sites = used.len();
let next = (0..num_sites).find(|&i| !used[i]);
match next {
None => results.push(current.clone()),
Some(site) => {
let candidates: Vec<usize> = adjacency[site]
.iter()
.copied()
.filter(|&nbr| !used[nbr] && !excluded.contains(&nbr))
.collect();
for nbr in candidates {
used[site] = true;
used[nbr] = true;
current.push((site.min(nbr), site.max(nbr)));
backtrack_matching(adjacency, used, current, results, excluded);
current.pop();
used[site] = false;
used[nbr] = false;
}
},
}
}
fn build_adjacency(num_sites: usize, bonds: &[(usize, usize)]) -> Result<Vec<Vec<usize>>> {
let mut adjacency = vec![Vec::new(); num_sites];
for &(i, j) in bonds {
if i >= num_sites || j >= num_sites {
return Err(error::invalid_param(
"bonds",
"bond references a site index out of range",
));
}
if i == j {
return Err(error::invalid_param(
"bonds",
"a bond cannot connect a site to itself",
));
}
adjacency[i].push(j);
adjacency[j].push(i);
}
Ok(adjacency)
}
pub(crate) fn bipartite_coloring(
num_sites: usize,
bonds: &[(usize, usize)],
) -> Result<Option<Vec<i8>>> {
let adjacency = build_adjacency(num_sites, bonds)?;
let mut color: Vec<i8> = vec![-1; num_sites];
for start in 0..num_sites {
if color[start] != -1 {
continue;
}
color[start] = 0;
let mut queue = std::collections::VecDeque::new();
queue.push_back(start);
while let Some(u) = queue.pop_front() {
for &v in &adjacency[u] {
if color[v] == -1 {
color[v] = 1 - color[u];
queue.push_back(v);
} else if color[v] == color[u] {
return Ok(None);
}
}
}
}
Ok(Some(color))
}
pub(crate) fn bfs_distances(
num_sites: usize,
bonds: &[(usize, usize)],
source: usize,
) -> Result<Vec<Option<usize>>> {
if source >= num_sites {
return Err(error::invalid_param(
"source",
"source site index out of range",
));
}
let adjacency = build_adjacency(num_sites, bonds)?;
let mut distances: Vec<Option<usize>> = vec![None; num_sites];
distances[source] = Some(0);
let mut queue = std::collections::VecDeque::new();
queue.push_back(source);
while let Some(u) = queue.pop_front() {
let du = distances[u].unwrap_or(0);
for &v in &adjacency[u] {
if distances[v].is_none() {
distances[v] = Some(du + 1);
queue.push_back(v);
}
}
}
Ok(distances)
}
#[cfg(test)]
mod tests {
use super::*;
fn ring4_bonds() -> Vec<(usize, usize)> {
vec![(0, 1), (1, 2), (2, 3), (3, 0)]
}
fn ladder_2x4_bonds() -> Vec<(usize, usize)> {
vec![
(0, 1),
(1, 2),
(2, 3),
(4, 5),
(5, 6),
(6, 7),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
]
}
fn square_4x4_bonds() -> Vec<(usize, usize)> {
let idx = |x: usize, y: usize| -> usize { y * 4 + x };
let mut bonds = Vec::new();
for y in 0..4 {
for x in 0..4 {
if x + 1 < 4 {
bonds.push((idx(x, y), idx(x + 1, y)));
}
if y + 1 < 4 {
bonds.push((idx(x, y), idx(x, y + 1)));
}
}
}
bonds
}
#[test]
fn test_dimer_covering_new_validates_double_use() {
let result = DimerCovering::new(4, vec![(0, 1), (1, 2)]);
assert!(result.is_err());
}
#[test]
fn test_dimer_covering_new_self_loop_errors() {
let result = DimerCovering::new(4, vec![(0, 0)]);
assert!(result.is_err());
}
#[test]
fn test_dimer_covering_monomers() {
let cov = DimerCovering::new(4, vec![(0, 1)]).expect("valid partial covering");
assert_eq!(cov.monomers, vec![2, 3]);
assert!(cov.is_paired(0, 1));
assert!(cov.is_paired(1, 0));
assert!(!cov.is_paired(0, 2));
assert_eq!(cov.partner_of(2), None);
}
#[test]
fn test_ring4_covering_count_is_two() {
let coverings = DimerCovering::enumerate_perfect_matchings(4, &ring4_bonds())
.expect("ring4 enumeration should succeed");
assert_eq!(
coverings.len(),
2,
"4-ring should have exactly 2 nearest-neighbor dimer coverings"
);
for cov in &coverings {
assert!(cov.monomers.is_empty());
}
}
#[test]
fn test_ladder_2x4_covering_count_is_five() {
let coverings = DimerCovering::enumerate_perfect_matchings(8, &ladder_2x4_bonds())
.expect("ladder enumeration should succeed");
assert_eq!(
coverings.len(),
5,
"2x4 ladder should have exactly 5 nearest-neighbor dimer coverings"
);
}
#[test]
fn test_square_4x4_covering_count_is_36() {
let coverings = DimerCovering::enumerate_perfect_matchings(16, &square_4x4_bonds())
.expect("4x4 open square enumeration should succeed");
assert_eq!(
coverings.len(),
36,
"4x4 open square should have exactly 36 nearest-neighbor dimer coverings"
);
}
#[test]
fn test_odd_num_sites_errors() {
let result = DimerCovering::enumerate_perfect_matchings(3, &[(0, 1), (1, 2)]);
assert!(result.is_err());
}
#[test]
fn test_monomer_dimer_ring4() {
let coverings = DimerCovering::enumerate_monomer_dimer_matchings(4, &ring4_bonds(), 0, 2)
.expect("enumeration should succeed even if empty");
assert!(coverings.is_empty());
let coverings = DimerCovering::enumerate_monomer_dimer_matchings(4, &ring4_bonds(), 0, 1)
.expect("enumeration should succeed");
assert_eq!(coverings.len(), 1);
assert_eq!(coverings[0].monomers, vec![0, 1]);
assert_eq!(coverings[0].dimers, vec![(2, 3)]);
}
#[test]
fn test_monomer_dimer_invalid_same_site() {
let result = DimerCovering::enumerate_monomer_dimer_matchings(4, &ring4_bonds(), 1, 1);
assert!(result.is_err());
}
#[test]
fn test_bipartite_coloring_ring4() {
let coloring = bipartite_coloring(4, &ring4_bonds())
.expect("bipartite check should not error")
.expect("4-ring is bipartite");
assert_ne!(coloring[0], coloring[1]);
assert_ne!(coloring[1], coloring[2]);
assert_eq!(coloring[0], coloring[2]);
}
#[test]
fn test_bipartite_coloring_triangle_is_none() {
let triangle_bonds = vec![(0, 1), (1, 2), (2, 0)];
let coloring = bipartite_coloring(3, &triangle_bonds).expect("check should not error");
assert!(
coloring.is_none(),
"a triangle (odd cycle) is not bipartite"
);
}
#[test]
fn test_bfs_distances_ring4() {
let distances = bfs_distances(4, &ring4_bonds(), 0).expect("bfs should succeed");
assert_eq!(distances[0], Some(0));
assert_eq!(distances[1], Some(1));
assert_eq!(distances[2], Some(2));
assert_eq!(distances[3], Some(1));
}
#[test]
fn test_bfs_distances_disconnected() {
let distances = bfs_distances(3, &[(0, 1)], 0).expect("bfs should succeed");
assert_eq!(distances[0], Some(0));
assert_eq!(distances[1], Some(1));
assert_eq!(distances[2], None);
}
#[test]
fn test_build_adjacency_out_of_range_errors() {
let result = DimerCovering::enumerate_perfect_matchings(4, &[(0, 5)]);
assert!(result.is_err());
}
}