use crate::domain::Domain;
use crate::variable::Variable;
use super::traits::{Revision, VarId};
type CageInt = i64;
fn cell_bounds<D: Domain>(
dom: &D,
to_int: fn(&D::Value) -> CageInt,
) -> Option<(CageInt, CageInt, bool)> {
let mut lo = CageInt::MAX;
let mut hi = CageInt::MIN;
let mut has_zero = false;
for val in dom.iter() {
let x = to_int(&val);
lo = lo.min(x);
hi = hi.max(x);
has_zero |= x == 0;
}
if hi < lo {
None
} else {
Some((lo, hi, has_zero))
}
}
pub struct CageSum<V> {
pub(crate) scope: Vec<VarId>,
target: CageInt,
to_int: fn(&V) -> CageInt,
}
impl<V> std::fmt::Debug for CageSum<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CageSum(target={}, {:?})", self.target, self.scope)
}
}
impl CageSum<u32> {
pub fn new(scope: Vec<VarId>, target: u32) -> Self {
Self {
scope,
target: target as CageInt,
to_int: |v| *v as CageInt,
}
}
}
impl<V> CageSum<V> {
pub(crate) fn check_impl(&self, assignment: &[Option<V>]) -> bool {
let mut sum: CageInt = 0;
for &v in &self.scope {
match &assignment[v as usize] {
Some(val) => sum += (self.to_int)(val),
None => return true,
}
}
sum == self.target
}
pub(crate) fn revise_impl<D: Domain<Value = V>>(
&self,
vars: &mut [Variable<D>],
depth: usize,
) -> Revision {
let mut changed = false;
loop {
let mut s_min: CageInt = 0;
let mut s_max: CageInt = 0;
for &v in &self.scope {
match cell_bounds(&vars[v as usize].domain, self.to_int) {
Some((lo, hi, _)) => {
s_min += lo;
s_max += hi;
}
None => return Revision::Unsatisfiable,
}
}
let mut pass_changed = false;
for &v in &self.scope {
let (lo_i, hi_i, _) = cell_bounds(&vars[v as usize].domain, self.to_int)
.expect("non-empty: totals pass above would have returned");
let allow_lo = self.target - (s_max - hi_i);
let allow_hi = self.target - (s_min - lo_i);
for val in vars[v as usize].domain.iter() {
let x = (self.to_int)(&val);
if (x < allow_lo || x > allow_hi) && vars[v as usize].prune(&val, depth) {
pass_changed = true;
changed = true;
}
}
if vars[v as usize].domain.is_empty() {
return Revision::Unsatisfiable;
}
}
if !pass_changed {
break;
}
}
if changed {
Revision::Changed
} else {
Revision::Unchanged
}
}
}
pub struct CageProduct<V> {
pub(crate) scope: Vec<VarId>,
target: CageInt,
to_int: fn(&V) -> CageInt,
}
impl<V> std::fmt::Debug for CageProduct<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CageProduct(target={}, {:?})", self.target, self.scope)
}
}
impl CageProduct<u32> {
pub fn new(scope: Vec<VarId>, target: u32) -> Self {
Self {
scope,
target: target as CageInt,
to_int: |v| *v as CageInt,
}
}
}
impl<V> CageProduct<V> {
pub(crate) fn check_impl(&self, assignment: &[Option<V>]) -> bool {
let mut prod: CageInt = 1;
for &v in &self.scope {
match &assignment[v as usize] {
Some(val) => prod = prod.saturating_mul((self.to_int)(val)),
None => return true,
}
}
prod == self.target
}
pub(crate) fn revise_impl<D: Domain<Value = V>>(
&self,
vars: &mut [Variable<D>],
depth: usize,
) -> Revision {
let mut changed = false;
loop {
for &v in &self.scope {
if cell_bounds(&vars[v as usize].domain, self.to_int).is_none() {
return Revision::Unsatisfiable;
}
}
let mut pass_changed = false;
for (i, &v) in self.scope.iter().enumerate() {
if self.target == 0 {
let peers_can_be_zero = self.scope.iter().enumerate().any(|(j, &u)| {
j != i
&& cell_bounds(&vars[u as usize].domain, self.to_int)
.is_some_and(|c| c.2)
});
if peers_can_be_zero {
continue;
}
for val in vars[v as usize].domain.iter() {
if (self.to_int)(&val) != 0 && vars[v as usize].prune(&val, depth) {
pass_changed = true;
changed = true;
}
}
} else {
let mut others_min: CageInt = 1;
let mut others_max: CageInt = 1;
for (j, &u) in self.scope.iter().enumerate() {
if j == i {
continue;
}
let (lo, hi, _) = cell_bounds(&vars[u as usize].domain, self.to_int)
.expect("non-empty: validity pass above would have returned");
others_min = others_min.saturating_mul(lo);
others_max = others_max.saturating_mul(hi);
}
for val in vars[v as usize].domain.iter() {
let x = (self.to_int)(&val);
let prune = x == 0
|| self.target % x != 0
|| !(others_min..=others_max).contains(&(self.target / x));
if prune && vars[v as usize].prune(&val, depth) {
pass_changed = true;
changed = true;
}
}
}
if vars[v as usize].domain.is_empty() {
return Revision::Unsatisfiable;
}
}
if !pass_changed {
break;
}
}
if changed {
Revision::Changed
} else {
Revision::Unchanged
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraint::LambdaConstraint;
use crate::constraint::traits::Constraint;
use crate::domain::BitsetDomain;
fn vars_from(domains: &[Vec<u32>]) -> Vec<Variable<BitsetDomain>> {
domains
.iter()
.map(|d| Variable::new(BitsetDomain::new(d.iter().copied())))
.collect()
}
fn rep(d: &[u32], n: usize) -> Vec<Vec<u32>> {
(0..n).map(|_| d.to_vec()).collect()
}
fn domain_of(v: &Variable<BitsetDomain>) -> Vec<u32> {
let mut vals = v.domain.values();
vals.sort_unstable();
vals
}
#[test]
fn n_ary_lambda_cage_sum_does_not_propagate() {
let mut vars = vars_from(&rep(&[1, 2, 3, 4, 5, 6, 7, 8, 9], 3));
let lambda = LambdaConstraint::new(
vec![0, 1, 2],
|a: &[Option<u32>]| match (a[0], a[1], a[2]) {
(Some(x), Some(y), Some(z)) => x + y + z == 6,
_ => true,
},
"cage_sum(6)",
);
let rev = Constraint::revise(&lambda, &mut vars, 1);
assert_eq!(
rev,
Revision::Unchanged,
"the n-ary lambda wall must be live"
);
for v in &vars {
assert_eq!(v.domain.size(), 9, "lambda pruned nothing — the wall");
}
}
#[test]
fn cage_sum_revise_prunes_by_residual() {
let mut vars = vars_from(&rep(&[1, 2, 3, 4, 5, 6, 7, 8, 9], 3));
let cage = CageSum::new(vec![0, 1, 2], 6);
let rev = cage.revise_impl(&mut vars, 1);
assert_eq!(rev, Revision::Changed);
for v in &vars {
assert_eq!(domain_of(v), vec![1, 2, 3, 4]);
}
}
#[test]
fn cage_sum_revise_tightens_low_end() {
let mut vars = vars_from(&rep(&[1, 2, 3, 4, 5, 6, 7, 8, 9], 2));
let cage = CageSum::new(vec![0, 1], 17);
assert_eq!(cage.revise_impl(&mut vars, 1), Revision::Changed);
assert_eq!(domain_of(&vars[0]), vec![8, 9]);
assert_eq!(domain_of(&vars[1]), vec![8, 9]);
}
#[test]
fn cage_sum_revise_detects_unsat() {
let mut vars = vars_from(&[vec![1, 2], vec![1, 2]]);
let cage = CageSum::new(vec![0, 1], 9);
assert_eq!(cage.revise_impl(&mut vars, 1), Revision::Unsatisfiable);
}
#[test]
fn cage_product_revise_prunes_by_divisibility_and_bound() {
let mut vars = vars_from(&rep(&[1, 2, 3, 4, 5, 6], 3));
let cage = CageProduct::new(vec![0, 1, 2], 6);
assert_eq!(cage.revise_impl(&mut vars, 1), Revision::Changed);
for v in &vars {
assert_eq!(domain_of(v), vec![1, 2, 3, 6]);
}
}
#[test]
fn cage_product_revise_prunes_zero_for_nonzero_target() {
let mut vars = vars_from(&[vec![0, 1, 2, 3], vec![0, 1, 2, 3]]);
let cage = CageProduct::new(vec![0, 1], 6);
assert_eq!(cage.revise_impl(&mut vars, 1), Revision::Changed);
assert_eq!(domain_of(&vars[0]), vec![2, 3]);
assert_eq!(domain_of(&vars[1]), vec![2, 3]);
}
#[test]
fn cage_product_revise_detects_unsat() {
let mut vars = vars_from(&[vec![1, 2], vec![1, 2]]);
let cage = CageProduct::new(vec![0, 1], 9);
assert_eq!(cage.revise_impl(&mut vars, 1), Revision::Unsatisfiable);
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0
}
fn below(&mut self, n: u32) -> u32 {
(self.next() >> 33) as u32 % n
}
}
fn random_domain(rng: &mut Lcg, pool: &[u32]) -> Vec<u32> {
loop {
let d: Vec<u32> = pool.iter().copied().filter(|_| rng.below(2) == 0).collect();
if !d.is_empty() {
return d;
}
}
}
fn supported(domains: &[Vec<u32>], keep: impl Fn(&[u32]) -> bool) -> Vec<Vec<u32>> {
let n = domains.len();
let mut supp: Vec<std::collections::BTreeSet<u32>> = vec![Default::default(); n];
let mut cur = vec![0u32; n];
fn rec(
i: usize,
domains: &[Vec<u32>],
cur: &mut Vec<u32>,
keep: &dyn Fn(&[u32]) -> bool,
supp: &mut [std::collections::BTreeSet<u32>],
) {
if i == domains.len() {
if keep(cur) {
for (k, &v) in cur.iter().enumerate() {
supp[k].insert(v);
}
}
return;
}
for &v in &domains[i] {
cur[i] = v;
rec(i + 1, domains, cur, keep, supp);
}
}
rec(0, domains, &mut cur, &keep, &mut supp);
supp.into_iter().map(|s| s.into_iter().collect()).collect()
}
fn assert_sound<F, R>(label: &str, domains: &[Vec<u32>], keep: F, revise: R)
where
F: Fn(&[u32]) -> bool,
R: Fn(&mut [Variable<BitsetDomain>]) -> Revision,
{
let supp = supported(domains, &keep);
let mut vars = vars_from(domains);
let rev = revise(&mut vars);
for (i, want) in supp.iter().enumerate() {
let got = domain_of(&vars[i]);
for v in want {
assert!(
got.contains(v),
"{label}: cell {i} pruned solution-supported value {v}\n domains={domains:?}\n survived={got:?}\n supported={want:?}",
);
}
for g in &got {
assert!(
domains[i].contains(g),
"{label}: cell {i} invented value {g}"
);
}
}
let unsatisfiable = supp.iter().any(|s| s.is_empty());
if unsatisfiable {
let emptied = vars.iter().any(|v| v.domain.is_empty());
assert!(
rev == Revision::Unsatisfiable || emptied || rev == Revision::Changed,
"{label}: unsatisfiable cage not flagged for domains={domains:?}"
);
}
}
#[test]
fn cage_sum_revise_soundness_randomized() {
let mut rng = Lcg(0x51ED_C0DE_u64);
let pool: Vec<u32> = (1..=6).collect();
for _ in 0..2000 {
let n = 2 + rng.below(3) as usize; let domains: Vec<Vec<u32>> = (0..n).map(|_| random_domain(&mut rng, &pool)).collect();
let target = 1 + rng.below(24) as i128; let scope: Vec<VarId> = (0..n as VarId).collect();
assert_sound(
"cage_sum",
&domains,
|c| c.iter().map(|&x| x as i128).sum::<i128>() == target,
|vars| CageSum::new(scope.clone(), target as u32).revise_impl(vars, 1),
);
}
}
#[test]
fn cage_product_revise_soundness_randomized() {
let mut rng = Lcg(0xC0FF_EE42_u64);
let pool: Vec<u32> = (0..=6).collect();
for _ in 0..2000 {
let n = 2 + rng.below(3) as usize;
let domains: Vec<Vec<u32>> = (0..n).map(|_| random_domain(&mut rng, &pool)).collect();
let target = rng.below(50) as i128; let scope: Vec<VarId> = (0..n as VarId).collect();
assert_sound(
"cage_product",
&domains,
|c| c.iter().map(|&x| x as i128).product::<i128>() == target,
|vars| CageProduct::new(scope.clone(), target as u32).revise_impl(vars, 1),
);
}
}
}