csp_solver/constraint/all_different_except.rs
1//! N-ary all-different constraint with a sentinel escape value.
2//!
3//! Parallel to [`AllDifferent`](super::all_different::AllDifferent) but permits
4//! arbitrarily many variables to share a single *sentinel* value — useful for
5//! modelling partial bipartite assignment, where a dedicated "unmatched" token
6//! may be selected by any number of sources simultaneously while every other
7//! (real) target must still be picked at most once.
8//!
9//! ```no_run
10//! use csp_solver::constraint::AllDifferentExcept;
11//! use csp_solver::domain::BitsetDomain;
12//! use csp_solver::Csp;
13//!
14//! // Four variables, each picking a target in 0..4. Value `0` is the
15//! // "unmatched" sentinel — any number of variables may land on it.
16//! let mut csp: Csp<BitsetDomain> = Csp::new();
17//! let vars = csp.add_variables(&BitsetDomain::range(4), 4);
18//! csp.add_constraint_enum(
19//! csp_solver::constraint::ConstraintEnum::AllDifferentExcept(
20//! AllDifferentExcept::new(vars, 0),
21//! ),
22//! );
23//! csp.finalize();
24//! ```
25
26use crate::domain::Domain;
27use crate::variable::Variable;
28
29use super::traits::{Constraint, Revision, VarId};
30
31/// All-different over the scope, *except* that the configured `sentinel`
32/// value may be assigned to any number of variables.
33///
34/// Non-sentinel assignments must remain pairwise distinct.
35#[derive(Debug)]
36pub struct AllDifferentExcept<V: Clone + PartialEq + std::fmt::Debug> {
37 pub(crate) scope: Vec<VarId>,
38 pub(crate) sentinel: V,
39}
40
41impl<V: Clone + PartialEq + std::fmt::Debug> AllDifferentExcept<V> {
42 /// Create a new all-different-except constraint over `vars`, treating
43 /// `sentinel` as the escape value that may be reused without conflict.
44 pub fn new(vars: Vec<VarId>, sentinel: V) -> Self {
45 Self { scope: vars, sentinel }
46 }
47
48 pub(crate) fn check_impl(&self, assignment: &[Option<V>]) -> bool {
49 // Collect non-sentinel assigned values; sentinel assignments are
50 // elided entirely so they never participate in the pairwise test.
51 let assigned: Vec<&V> = self
52 .scope
53 .iter()
54 .filter_map(|&v| assignment[v as usize].as_ref())
55 .filter(|val| **val != self.sentinel)
56 .collect();
57 for i in 0..assigned.len() {
58 for j in (i + 1)..assigned.len() {
59 if assigned[i] == assigned[j] {
60 return false;
61 }
62 }
63 }
64 true
65 }
66
67 /// Prune non-sentinel values from peer domains.
68 ///
69 /// For small scopes (< 4 variables), uses fast singleton removal:
70 /// a variable pinned to the sentinel does not forbid peers from
71 /// choosing the sentinel themselves, while non-sentinel singletons
72 /// behave exactly like [`AllDifferent`](super::all_different::AllDifferent).
73 ///
74 /// For larger scopes (≥ 4), delegates to Régin's GAC algorithm
75 /// ([`propagate_gac_alldiff_except`](crate::solver::gac_alldiff_except::propagate_gac_alldiff_except)),
76 /// which achieves generalized arc consistency by building a bipartite
77 /// matching graph over non-sentinel values and pruning values that
78 /// cannot participate in any maximum matching.
79 pub(crate) fn revise_impl<D>(&self, vars: &mut [Variable<D>], depth: usize) -> Revision
80 where
81 D: Domain<Value = V>,
82 {
83 // For larger scopes, GAC provides strictly stronger pruning than
84 // singleton removal — it detects Hall sets and matching failures
85 // that pairwise checks miss.
86 if self.scope.len() >= 4 {
87 return crate::solver::gac_alldiff_except::propagate_gac_alldiff_except(
88 &self.scope,
89 &self.sentinel,
90 vars,
91 depth,
92 );
93 }
94
95 // Small scope: singleton removal is fast and sufficient.
96 let mut changed = false;
97 let singletons: Vec<(VarId, D::Value)> = self
98 .scope
99 .iter()
100 .filter_map(|&v| vars[v as usize].domain.singleton_value().map(|val| (v, val)))
101 .filter(|(_, val)| *val != self.sentinel)
102 .collect();
103
104 for (sv, sval) in &singletons {
105 for &other in &self.scope {
106 if other == *sv { continue; }
107 if vars[other as usize].prune(sval, depth) { changed = true; }
108 if vars[other as usize].domain.is_empty() { return Revision::Unsatisfiable; }
109 }
110 }
111
112 if changed { Revision::Changed } else { Revision::Unchanged }
113 }
114}
115
116impl<D> Constraint<D> for AllDifferentExcept<D::Value>
117where
118 D: Domain,
119 D::Value: PartialEq,
120{
121 fn scope(&self) -> &[VarId] { &self.scope }
122 fn check(&self, assignment: &[Option<D::Value>]) -> bool { self.check_impl(assignment) }
123 fn revise(&self, vars: &mut [Variable<D>], depth: usize) -> Revision {
124 self.revise_impl(vars, depth)
125 }
126}