Skip to main content

problemreductions/models/set/
minimum_hitting_set.rs

1//! Minimum Hitting Set problem implementation.
2//!
3//! The Minimum Hitting Set problem asks for a minimum-size subset of universe
4//! elements that intersects every set in a collection.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Min;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "MinimumHittingSet",
14        display_name: "Minimum Hitting Set",
15        aliases: &[],
16        dimensions: &[],
17        category: crate::registry::ProblemCategory::Set,
18        module_path: module_path!(),
19        description: "Find a minimum-size subset of universe elements that hits every set",
20        fields: MinimumHittingSetCreateSpec::FIELDS,
21    }
22}
23
24/// The Minimum Hitting Set problem.
25///
26/// Given a universe `U` and a collection of subsets of `U`, find a minimum-size
27/// subset `H ⊆ U` such that `H` intersects every set in the collection.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct MinimumHittingSet {
30    universe_size: usize,
31    sets: Vec<Vec<usize>>,
32}
33
34#[derive(Debug, Deserialize, crate::CreateSpec)]
35struct MinimumHittingSetCreateSpec {
36    /// Size of the universe U.
37    universe_size: usize,
38    /// Collection of subsets of U that must each be hit.
39    subsets: Vec<Vec<usize>>,
40}
41
42impl TryFrom<MinimumHittingSetCreateSpec> for MinimumHittingSet {
43    type Error = crate::registry::ConstructionError;
44
45    fn try_from(spec: MinimumHittingSetCreateSpec) -> Result<Self, Self::Error> {
46        for (set_index, set) in spec.subsets.iter().enumerate() {
47            if let Some(&element) = set.iter().find(|&&element| element >= spec.universe_size) {
48                return Err(format!(
49                    "subsets[{set_index}] contains element {element} outside universe of size {}",
50                    spec.universe_size
51                )
52                .into());
53            }
54        }
55        Ok(Self::new(spec.universe_size, spec.subsets))
56    }
57}
58
59impl MinimumHittingSet {
60    /// Create a new Minimum Hitting Set instance.
61    ///
62    /// # Panics
63    ///
64    /// Panics if any set contains an element outside `0..universe_size`.
65    pub fn new(universe_size: usize, sets: Vec<Vec<usize>>) -> Self {
66        let mut sets = sets;
67        for (set_index, set) in sets.iter_mut().enumerate() {
68            set.sort_unstable();
69            set.dedup();
70            for &element in set.iter() {
71                assert!(
72                    element < universe_size,
73                    "Set {set_index} contains element {element} which is outside universe of size {universe_size}"
74                );
75            }
76        }
77
78        Self {
79            universe_size,
80            sets,
81        }
82    }
83
84    /// Get the universe size.
85    pub fn universe_size(&self) -> usize {
86        self.universe_size
87    }
88
89    /// Get the number of sets.
90    pub fn num_sets(&self) -> usize {
91        self.sets.len()
92    }
93
94    /// Get the sets.
95    pub fn sets(&self) -> &[Vec<usize>] {
96        &self.sets
97    }
98
99    /// Get a specific set.
100    pub fn get_set(&self, index: usize) -> Option<&Vec<usize>> {
101        self.sets.get(index)
102    }
103
104    /// Decode the selected universe elements from a binary configuration.
105    pub fn selected_elements(&self, config: &[bool]) -> Option<Vec<usize>> {
106        if config.len() != self.universe_size {
107            return None;
108        }
109
110        let mut selected = Vec::new();
111        for (element, &is_selected) in config.iter().enumerate() {
112            if is_selected {
113                selected.push(element);
114            }
115        }
116        Some(selected)
117    }
118
119    /// Check whether a configuration hits every set in the collection.
120    pub fn is_valid_solution(&self, config: &[bool]) -> bool {
121        let Some(selected) = self.selected_elements(config) else {
122            return false;
123        };
124
125        self.sets.iter().all(|set| {
126            set.iter()
127                .any(|element| selected.binary_search(element).is_ok())
128        })
129    }
130}
131
132impl Problem for MinimumHittingSet {
133    const NAME: &'static str = "MinimumHittingSet";
134    type Solution = Vec<bool>;
135    type Value = Min<i64>;
136
137    crate::problem_parameters![("num_sets", num_sets), ("universe_size", universe_size),];
138
139    fn evaluate(
140        &self,
141        config: &Self::Solution,
142    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
143        Ok({
144            let Some(selected) = self.selected_elements(config) else {
145                return Err(crate::traits::EvaluationError::InvalidConfiguration(
146                    "element-selection length does not match the universe".into(),
147                ));
148            };
149
150            if self.sets.iter().all(|set| {
151                set.iter()
152                    .any(|element| selected.binary_search(element).is_ok())
153            }) {
154                Min(Some(i64::try_from(selected.len()).map_err(|_| {
155                    crate::traits::EvaluationError::IntegerOverflow(
156                        "converting hitting-set cardinality to i64".into(),
157                    )
158                })?))
159            } else {
160                Min(None)
161            }
162        })
163    }
164
165    fn variant() -> Vec<(&'static str, &'static str)> {
166        crate::variant_params![]
167    }
168}
169
170impl crate::solvers::BruteForceProblem for MinimumHittingSet {
171    fn dimensions(&self) -> Vec<usize> {
172        vec![2; self.universe_size]
173    }
174}
175
176crate::declare_variants! {
177    default MinimumHittingSet => "2^universe_size" create MinimumHittingSetCreateSpec,
178}
179
180crate::register_brute_force! {
181    MinimumHittingSet decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
182}
183
184#[cfg(feature = "example-db")]
185pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
186    vec![crate::example_db::specs::ModelExampleSpec {
187        id: "minimum_hitting_set",
188        instance: Box::new(MinimumHittingSet::new(
189            6,
190            vec![
191                vec![0, 1, 2],
192                vec![0, 3, 4],
193                vec![1, 3, 5],
194                vec![2, 4, 5],
195                vec![0, 1, 5],
196                vec![2, 3],
197                vec![1, 4],
198            ],
199        )),
200        optimal_config: serde_json::json!(vec![false, true, false, true, true, false]),
201        optimal_value: serde_json::json!(3),
202    }]
203}
204
205#[cfg(test)]
206#[path = "../../unit_tests/models/set/minimum_hitting_set.rs"]
207mod tests;