csp_solver/domain/cost_finite.rs
1//! CostFiniteDomain — finite integer domain with parallel per-value cost vector.
2//!
3//! Wraps `FiniteDomain<i32>` with a sorted `(value, cost)` table so that
4//! `CostDomain::cost` is O(log n) via binary search. The cost table is
5//! immutable from construction; removals prune the underlying
6//! `FiniteDomain<i32>` but leave the table untouched so that the surviving
7//! entries keep their original costs across `remove`/`add` cycles.
8//!
9//! # Why `i32` and not `u32`?
10//!
11//! Bipartite-assignment COPs routinely need a "no match" sentinel value
12//! distinct from every valid target index. Using `i32` lets the sentinel
13//! be `-1` — a natural, immediately-recognizable marker — without
14//! consuming a valid index or widening the type. Clients that only need
15//! non-negative values are free to pass them; the domain is agnostic.
16//!
17//! # Typical assignment-COP usage
18//!
19//! ```
20//! use csp_solver::domain::CostFiniteDomain;
21//! use csp_solver::domain::traits::{CostDomain, Domain};
22//!
23//! // Row i of a 3×3 cost matrix, with a −1 sentinel priced at the
24//! // unmatched penalty. Each source row gets its own domain built from
25//! // the corresponding slice of the cost matrix.
26//! let cost_row = [4.2, 1.1, 7.8];
27//! let unmatch_cost = 100.0;
28//!
29//! let values = vec![-1_i32, 0, 1, 2];
30//! let costs = vec![unmatch_cost, cost_row[0], cost_row[1], cost_row[2]];
31//! let domain = CostFiniteDomain::new(values, costs);
32//!
33//! assert_eq!(domain.size(), 4);
34//! assert_eq!(domain.cost(&1), 1.1);
35//! assert_eq!(domain.cost(&-1), unmatch_cost);
36//! assert_eq!(domain.min_cost(), 1.1);
37//! ```
38
39use std::cell::Cell;
40
41use super::finite::FiniteDomain;
42use super::traits::{CostDomain, Domain};
43
44/// A finite integer domain paired with a per-value cost vector.
45///
46/// Construction takes `(values, costs)` in any order; the constructor
47/// canonicalizes to ascending value order so that `cost()` can binary
48/// search in O(log n). Removals shrink the active domain but leave the
49/// cost table alone, so values removed during search retain their
50/// original costs if they're added back later.
51///
52/// All `Domain` methods delegate to the wrapped `FiniteDomain<i32>`.
53/// `CostDomain::cost` looks the value up in the sorted cost table;
54/// `CostDomain::min_cost` uses a `Cell`-cached minimum that is
55/// lazily recomputed after any `remove`/`add` mutation — amortized
56/// O(1) for the repeated lower-bound queries the branch-and-bound
57/// solver issues at every search node.
58#[derive(Debug, Clone)]
59pub struct CostFiniteDomain {
60 /// Active values, pruned by the solver during search.
61 inner: FiniteDomain<i32>,
62 /// `(value, cost)` pairs sorted ascending by value. All values
63 /// present in `inner` are also in this table; values removed from
64 /// `inner` may remain here harmlessly because `cost()` is only
65 /// ever queried for live values.
66 costs: Vec<(i32, f64)>,
67 /// Lazily cached minimum cost over the live domain. `None` means
68 /// "dirty — recompute on next `min_cost()` call". Invalidated by
69 /// `remove`/`add`; populated by `min_cost`.
70 min_cost_cache: Cell<Option<f64>>,
71}
72
73impl PartialEq for CostFiniteDomain {
74 fn eq(&self, other: &Self) -> bool {
75 self.inner == other.inner && self.costs == other.costs
76 }
77}
78
79impl CostFiniteDomain {
80 /// Construct a new cost-carrying domain from parallel `values` and
81 /// `costs` slices.
82 ///
83 /// The two inputs are zipped element-wise, then sorted by value so
84 /// that the internal cost table is canonical regardless of input
85 /// order. Duplicate values are not deduplicated — callers are
86 /// responsible for passing a unique value set.
87 ///
88 /// # Panics
89 ///
90 /// Panics if `values.len() != costs.len()`.
91 pub fn new(values: Vec<i32>, costs: Vec<f64>) -> Self {
92 assert_eq!(
93 values.len(),
94 costs.len(),
95 "CostFiniteDomain::new: values and costs length mismatch ({} vs {})",
96 values.len(),
97 costs.len(),
98 );
99 let mut pairs: Vec<(i32, f64)> = values.into_iter().zip(costs).collect();
100 pairs.sort_by_key(|(v, _)| *v);
101 let sorted_values: Vec<i32> = pairs.iter().map(|(v, _)| *v).collect();
102 Self {
103 inner: FiniteDomain::new(sorted_values),
104 costs: pairs,
105 min_cost_cache: Cell::new(None),
106 }
107 }
108}
109
110impl Domain for CostFiniteDomain {
111 type Value = i32;
112
113 fn size(&self) -> usize {
114 self.inner.size()
115 }
116
117 fn contains(&self, val: &i32) -> bool {
118 self.inner.contains(val)
119 }
120
121 fn remove(&mut self, val: &i32) -> bool {
122 let changed = self.inner.remove(val);
123 if changed {
124 self.min_cost_cache.set(None);
125 }
126 changed
127 }
128
129 fn add(&mut self, val: &i32) {
130 self.inner.add(val);
131 self.min_cost_cache.set(None);
132 }
133
134 fn values(&self) -> Vec<i32> {
135 self.inner.values()
136 }
137
138 fn iter(&self) -> impl Iterator<Item = i32> {
139 self.inner.iter()
140 }
141}
142
143impl CostDomain for CostFiniteDomain {
144 /// Lookup cost for `val` via binary search over the sorted table.
145 ///
146 /// Returns `f64::INFINITY` if the value was never registered at
147 /// construction time. This is defensive — the solver only ever
148 /// queries values it read out of the domain, so a miss indicates
149 /// a client contract violation and the infinity acts as a soft
150 /// fence (the branch-and-bound search will reject it).
151 fn cost(&self, val: &i32) -> f64 {
152 match self.costs.binary_search_by(|(v, _)| v.cmp(val)) {
153 Ok(idx) => self.costs[idx].1,
154 Err(_) => f64::INFINITY,
155 }
156 }
157
158 /// Amortized O(1) minimum cost over the live domain.
159 ///
160 /// The result is lazily cached in a `Cell` and invalidated by
161 /// `remove`/`add`. The branch-and-bound solver calls `min_cost()`
162 /// once per unassigned variable per search node, so caching
163 /// eliminates the dominant O(domain) scan at large domain sizes.
164 fn min_cost(&self) -> f64 {
165 if let Some(cached) = self.min_cost_cache.get() {
166 return cached;
167 }
168 let min = self
169 .inner
170 .iter()
171 .map(|v| self.cost(&v))
172 .fold(f64::INFINITY, f64::min);
173 self.min_cost_cache.set(Some(min));
174 min
175 }
176}