Skip to main content

problemreductions/models/set/
comparative_containment.rs

1//! Comparative Containment problem implementation.
2//!
3//! Given two weighted families of sets over a common universe, determine
4//! whether there exists a subset of the universe whose containment weight
5//! in the first family is at least its containment weight in the second.
6
7use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension};
8use crate::traits::Problem;
9use crate::types::{One, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "ComparativeContainment",
16        display_name: "Comparative Containment",
17        aliases: &[],
18        dimensions: &[VariantDimension::new("weight", "i64", &["One", "i64", "f64"])],
19        category: crate::registry::ProblemCategory::Set,
20        module_path: module_path!(),
21        description: "Compare containment-weight sums for two set families over a shared universe",
22        fields: ComparativeContainmentI64CreateSpec::FIELDS,
23    }
24}
25
26/// Comparative Containment.
27///
28/// Given a universe `X`, two set families `R` and `S`, and positive weights
29/// on those sets, determine whether there exists a subset `Y ⊆ X` such that
30/// the total weight of `R`-sets containing `Y` is at least the total weight
31/// of `S`-sets containing `Y`.
32#[derive(Debug, Clone, Serialize)]
33pub struct ComparativeContainment<W = i64> {
34    universe_size: usize,
35    r_sets: Vec<Vec<usize>>,
36    s_sets: Vec<Vec<usize>>,
37    r_weights: Vec<W>,
38    s_weights: Vec<W>,
39}
40
41macro_rules! comparative_containment_create_spec {
42    ($name:ident, $weight:ty, $one:expr $(, $r_weights:ident, $s_weights:ident)?) => {
43        #[derive(Debug, Deserialize, crate::CreateSpec)]
44        struct $name {
45            /// Size of the common universe.
46            universe_size: usize,
47            /// First set family.
48            #[create(codec = "semicolon-separated")]
49            r_sets: Vec<Vec<usize>>,
50            /// Second set family.
51            #[create(codec = "semicolon-separated")]
52            s_sets: Vec<Vec<usize>>,
53            $(
54            /// Positive weights for the first family; defaults to one.
55            #[create(codec = "comma-separated")]
56            $r_weights: Option<Vec<$weight>>,
57            )?
58            $(
59            /// Positive weights for the second family; defaults to one.
60            #[create(codec = "comma-separated")]
61            $s_weights: Option<Vec<$weight>>,
62            )?
63        }
64
65        impl TryFrom<$name> for ComparativeContainment<$weight> {
66            type Error = ConstructionError;
67            fn try_from(spec: $name) -> Result<Self, Self::Error> {
68                let r_weights = { $(if let Some(value) = spec.$r_weights { value } else)? { vec![$one; spec.r_sets.len()] } };
69                let s_weights = { $(if let Some(value) = spec.$s_weights { value } else)? { vec![$one; spec.s_sets.len()] } };
70                ComparativeContainment::with_weights(
71                    spec.universe_size,
72                    spec.r_sets,
73                    spec.s_sets,
74                    r_weights,
75                    s_weights,
76                )
77            }
78        }
79    };
80}
81
82fn validate_create_set_family(
83    label: &str,
84    universe_size: usize,
85    sets: &[Vec<usize>],
86) -> Result<(), ConstructionError> {
87    for (set_index, set) in sets.iter().enumerate() {
88        for &element in set {
89            if element >= universe_size {
90                return Err(ConstructionError::Conversion(format!("{label} set {set_index} contains element {element} outside universe of size {universe_size}")));
91            }
92        }
93    }
94    Ok(())
95}
96
97fn validate_create_weights<W: WeightElement>(
98    label: &str,
99    count: usize,
100    weights: &[W],
101) -> Result<(), ConstructionError> {
102    if weights.len() != count {
103        return Err(ConstructionError::Conversion(format!(
104            "number of {label} sets and weights must match"
105        )));
106    }
107    for (index, weight) in weights.iter().enumerate() {
108        match weight.to_sum().partial_cmp(&W::Sum::zero()) {
109            None => {
110                return Err(ConstructionError::NonFiniteFloat(format!(
111                    "{label} weight at index {index} must be finite"
112                )));
113            }
114            Some(std::cmp::Ordering::Greater) => {}
115            Some(_) => {
116                return Err(ConstructionError::Conversion(format!(
117                    "{label} weight at index {index} must be positive"
118                )));
119            }
120        }
121    }
122    Ok(())
123}
124
125comparative_containment_create_spec!(
126    ComparativeContainmentI64CreateSpec,
127    i64,
128    1_i64,
129    r_weights,
130    s_weights
131);
132comparative_containment_create_spec!(
133    ComparativeContainmentF64CreateSpec,
134    f64,
135    1.0_f64,
136    r_weights,
137    s_weights
138);
139comparative_containment_create_spec!(ComparativeContainmentOneCreateSpec, One, One);
140
141impl<W: WeightElement> ComparativeContainment<W> {
142    /// Create a new instance with unit weights.
143    pub fn new(
144        universe_size: usize,
145        r_sets: Vec<Vec<usize>>,
146        s_sets: Vec<Vec<usize>>,
147    ) -> Result<Self, ConstructionError>
148    where
149        W: WeightElement,
150    {
151        let r_weights = vec![W::unit(); r_sets.len()];
152        let s_weights = vec![W::unit(); s_sets.len()];
153        Self::with_weights(universe_size, r_sets, s_sets, r_weights, s_weights)
154    }
155
156    /// Create a new instance with explicit weights.
157    pub fn with_weights(
158        universe_size: usize,
159        r_sets: Vec<Vec<usize>>,
160        s_sets: Vec<Vec<usize>>,
161        r_weights: Vec<W>,
162        s_weights: Vec<W>,
163    ) -> Result<Self, ConstructionError> {
164        validate_create_set_family("R", universe_size, &r_sets)?;
165        validate_create_set_family("S", universe_size, &s_sets)?;
166        validate_create_weights("R", r_sets.len(), &r_weights)?;
167        validate_create_weights("S", s_sets.len(), &s_weights)?;
168        Ok(Self {
169            universe_size,
170            r_sets,
171            s_sets,
172            r_weights,
173            s_weights,
174        })
175    }
176
177    /// Get the size of the universe.
178    pub fn universe_size(&self) -> usize {
179        self.universe_size
180    }
181
182    /// Get the number of sets in the R family.
183    pub fn num_r_sets(&self) -> usize {
184        self.r_sets.len()
185    }
186
187    /// Get the number of sets in the S family.
188    pub fn num_s_sets(&self) -> usize {
189        self.s_sets.len()
190    }
191
192    /// Get the R family.
193    pub fn r_sets(&self) -> &[Vec<usize>] {
194        &self.r_sets
195    }
196
197    /// Get the S family.
198    pub fn s_sets(&self) -> &[Vec<usize>] {
199        &self.s_sets
200    }
201
202    /// Get the R-family weights.
203    pub fn r_weights(&self) -> &[W] {
204        &self.r_weights
205    }
206
207    /// Get the S-family weights.
208    pub fn s_weights(&self) -> &[W] {
209        &self.s_weights
210    }
211
212    /// Check whether the subset selected by `config` is contained in `set`.
213    pub fn contains_selected_subset(&self, config: &[bool], set: &[usize]) -> bool {
214        self.valid_config(config) && contains_selected_subset_unchecked(config, set)
215    }
216
217    fn valid_config(&self, config: &[bool]) -> bool {
218        config.len() == self.universe_size
219    }
220}
221
222impl<'de, W> Deserialize<'de> for ComparativeContainment<W>
223where
224    W: WeightElement + Deserialize<'de>,
225{
226    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227    where
228        D: serde::Deserializer<'de>,
229    {
230        #[derive(Deserialize)]
231        struct Raw<W> {
232            universe_size: usize,
233            r_sets: Vec<Vec<usize>>,
234            s_sets: Vec<Vec<usize>>,
235            r_weights: Vec<W>,
236            s_weights: Vec<W>,
237        }
238
239        let raw = Raw::deserialize(deserializer)?;
240        Self::with_weights(
241            raw.universe_size,
242            raw.r_sets,
243            raw.s_sets,
244            raw.r_weights,
245            raw.s_weights,
246        )
247        .map_err(serde::de::Error::custom)
248    }
249}
250
251impl<W> ComparativeContainment<W>
252where
253    W: WeightElement,
254{
255    /// Total R-family weight for sets containing the selected subset.
256    pub fn r_weight_sum(
257        &self,
258        config: &[bool],
259    ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
260        self.sum_containing_weights(config, &self.r_sets, &self.r_weights)
261    }
262
263    /// Total S-family weight for sets containing the selected subset.
264    pub fn s_weight_sum(
265        &self,
266        config: &[bool],
267    ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
268        self.sum_containing_weights(config, &self.s_sets, &self.s_weights)
269    }
270
271    /// Check if a configuration is a satisfying solution.
272    pub fn is_valid_solution(
273        &self,
274        config: &[bool],
275    ) -> Result<bool, crate::traits::EvaluationError> {
276        Ok(
277            match (self.r_weight_sum(config)?, self.s_weight_sum(config)?) {
278                (Some(r_total), Some(s_total)) => r_total >= s_total,
279                _ => false,
280            },
281        )
282    }
283
284    fn sum_containing_weights(
285        &self,
286        config: &[bool],
287        sets: &[Vec<usize>],
288        weights: &[W],
289    ) -> Result<Option<W::Sum>, crate::traits::EvaluationError> {
290        if !self.valid_config(config) {
291            return Ok(None);
292        }
293
294        let mut total = W::Sum::zero();
295        for (set, weight) in sets.iter().zip(weights.iter()) {
296            if contains_selected_subset_unchecked(config, set) {
297                total = W::checked_add_to_sum(
298                    total,
299                    weight.to_sum(),
300                    "summing comparative containment weights",
301                )?;
302            }
303        }
304        Ok(Some(total))
305    }
306}
307
308impl<W> Problem for ComparativeContainment<W>
309where
310    W: WeightElement + crate::variant::VariantParam,
311{
312    const NAME: &'static str = "ComparativeContainment";
313    type Solution = Vec<bool>;
314    type Value = crate::types::Or;
315
316    crate::problem_parameters![
317        ("num_r_sets", num_r_sets),
318        ("num_s_sets", num_s_sets),
319        ("universe_size", universe_size),
320    ];
321
322    fn evaluate(
323        &self,
324        config: &Self::Solution,
325    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
326        if config.len() != self.universe_size {
327            return Err(crate::traits::EvaluationError::InvalidConfiguration(
328                "element-selection length does not match the universe".into(),
329            ));
330        }
331        Ok(crate::types::Or(self.is_valid_solution(config)?))
332    }
333
334    fn variant() -> Vec<(&'static str, &'static str)> {
335        crate::variant_params![W]
336    }
337}
338
339impl<W> crate::solvers::BruteForceProblem for ComparativeContainment<W>
340where
341    W: WeightElement + crate::variant::VariantParam,
342{
343    fn dimensions(&self) -> Vec<usize> {
344        vec![2; self.universe_size]
345    }
346}
347
348crate::declare_variants! {
349    ComparativeContainment<One> => "2^universe_size" create ComparativeContainmentOneCreateSpec,
350    default ComparativeContainment<i64> => "2^universe_size" create ComparativeContainmentI64CreateSpec,
351    ComparativeContainment<f64> => "2^universe_size" create ComparativeContainmentF64CreateSpec,
352}
353
354crate::register_brute_force! {
355    ComparativeContainment<One> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
356    ComparativeContainment<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
357    ComparativeContainment<f64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
358}
359
360fn contains_selected_subset_unchecked(config: &[bool], set: &[usize]) -> bool {
361    config
362        .iter()
363        .enumerate()
364        .all(|(element, &selected)| !selected || set.contains(&element))
365}
366
367#[cfg(feature = "example-db")]
368pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
369    vec![crate::example_db::specs::ModelExampleSpec {
370        id: "comparative_containment",
371        instance: Box::new(
372            ComparativeContainment::with_weights(
373                4,
374                vec![vec![0, 1, 2, 3], vec![0, 1]],
375                vec![vec![0, 1, 2, 3], vec![2, 3]],
376                vec![2, 5],
377                vec![3, 6],
378            )
379            .expect("canonical comparative-containment instance must be valid"),
380        ),
381        optimal_config: serde_json::json!(vec![false, true, false, false]),
382        optimal_value: serde_json::json!(true),
383    }]
384}
385
386#[cfg(test)]
387#[path = "../../unit_tests/models/set/comparative_containment.rs"]
388mod tests;