problemreductions/models/misc/
minimum_decision_tree.rs1use crate::registry::{CreateSpec, ProblemSchemaEntry};
8use crate::traits::Problem;
9use crate::types::Min;
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13 ProblemSchemaEntry {
14 name: "MinimumDecisionTree",
15 display_name: "Minimum Decision Tree",
16 aliases: &[],
17 dimensions: &[],
18 category: crate::registry::ProblemCategory::Misc,
19 module_path: module_path!(),
20 description: "Find decision tree identifying objects with minimum total path length",
21 fields: MinimumDecisionTreeCreateSpec::FIELDS,
22 }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct MinimumDecisionTree {
54 test_matrix: Vec<Vec<bool>>,
56 num_objects: usize,
58 num_tests: usize,
60}
61
62#[derive(Debug, Deserialize, crate::CreateSpec)]
63struct MinimumDecisionTreeCreateSpec {
64 #[create(codec = "json")]
66 test_matrix: Vec<Vec<bool>>,
67 num_objects: usize,
69 num_tests: usize,
71}
72
73impl TryFrom<MinimumDecisionTreeCreateSpec> for MinimumDecisionTree {
74 type Error = crate::registry::ConstructionError;
75 fn try_from(spec: MinimumDecisionTreeCreateSpec) -> Result<Self, Self::Error> {
76 if spec.num_objects < 2 {
77 return Err("num_objects must be at least 2".into());
78 }
79 if spec.num_tests == 0 {
80 return Err("num_tests must be positive".into());
81 }
82 if spec.test_matrix.len() != spec.num_tests {
83 return Err("test_matrix row count must equal num_tests".into());
84 }
85 if spec
86 .test_matrix
87 .iter()
88 .any(|row| row.len() != spec.num_objects)
89 {
90 return Err("each test_matrix row must have num_objects columns".into());
91 }
92 for a in 0..spec.num_objects {
93 for b in a + 1..spec.num_objects {
94 if !(0..spec.num_tests)
95 .any(|test| spec.test_matrix[test][a] != spec.test_matrix[test][b])
96 {
97 return Err(
98 format!("objects {a} and {b} are not distinguished by any test").into(),
99 );
100 }
101 }
102 }
103 Ok(Self {
104 test_matrix: spec.test_matrix,
105 num_objects: spec.num_objects,
106 num_tests: spec.num_tests,
107 })
108 }
109}
110
111impl MinimumDecisionTree {
112 pub fn new(test_matrix: Vec<Vec<bool>>, num_objects: usize, num_tests: usize) -> Self {
119 assert!(num_objects >= 2, "Need at least 2 objects");
120 assert!(num_tests >= 1, "Need at least 1 test");
121 assert_eq!(
122 test_matrix.len(),
123 num_tests,
124 "test_matrix must have num_tests rows"
125 );
126 for (j, row) in test_matrix.iter().enumerate() {
127 assert_eq!(
128 row.len(),
129 num_objects,
130 "test_matrix[{j}] must have num_objects columns"
131 );
132 }
133 for a in 0..num_objects {
135 for b in (a + 1)..num_objects {
136 let distinguished = (0..num_tests).any(|j| test_matrix[j][a] != test_matrix[j][b]);
137 assert!(
138 distinguished,
139 "Objects {a} and {b} are not distinguished by any test"
140 );
141 }
142 }
143 Self {
144 test_matrix,
145 num_objects,
146 num_tests,
147 }
148 }
149
150 pub fn num_objects(&self) -> usize {
152 self.num_objects
153 }
154
155 pub fn num_tests(&self) -> usize {
157 self.num_tests
158 }
159
160 pub fn test_matrix(&self) -> &[Vec<bool>] {
162 &self.test_matrix
163 }
164
165 fn num_tree_slots(&self) -> usize {
167 (1usize << (self.num_objects - 1)) - 1
168 }
169
170 fn leaf_sentinel(&self) -> usize {
172 self.num_tests
173 }
174
175 fn simulate(&self, config: &[usize]) -> Result<Option<i64>, crate::traits::EvaluationError> {
178 let sentinel = self.leaf_sentinel();
179 let max_slots = self.num_tree_slots();
180 let mut seen_leaves = std::collections::HashSet::new();
181 let mut total_depth = 0_i64;
182
183 for obj in 0..self.num_objects {
184 let mut node = 0usize;
185 let mut depth = 0usize;
186
187 loop {
188 if node >= max_slots || config[node] == sentinel {
189 if !seen_leaves.insert(node) {
191 return Ok(None);
192 }
193 let depth = i64::try_from(depth).map_err(|_| {
194 crate::traits::EvaluationError::IntegerOverflow(
195 "converting decision-tree depth to i64".to_string(),
196 )
197 })?;
198 total_depth = total_depth.checked_add(depth).ok_or_else(|| {
199 crate::traits::EvaluationError::IntegerOverflow(
200 "summing decision-tree external path length".to_string(),
201 )
202 })?;
203 break;
204 }
205
206 let test_idx = config[node];
207 debug_assert!(test_idx < self.num_tests);
208
209 let result = self.test_matrix[test_idx][obj];
210 node = if result { 2 * node + 2 } else { 2 * node + 1 };
211 depth += 1;
212
213 if depth > self.num_objects {
214 return Ok(None);
215 }
216 }
217 }
218
219 Ok(Some(total_depth))
220 }
221}
222
223impl Problem for MinimumDecisionTree {
224 const NAME: &'static str = "MinimumDecisionTree";
225 type Solution = Vec<usize>;
226 type Value = Min<i64>;
227
228 crate::problem_parameters![("num_objects", num_objects), ("num_tests", num_tests),];
229
230 fn evaluate(
231 &self,
232 config: &Self::Solution,
233 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
234 Ok({
235 if config.len() != self.num_tree_slots() {
236 return Err(crate::traits::EvaluationError::InvalidConfiguration(
237 "decision-tree encoding length does not match the instance".into(),
238 ));
239 }
240 Min(self.simulate(config)?)
241 })
242 }
243
244 fn variant() -> Vec<(&'static str, &'static str)> {
245 crate::variant_params![]
246 }
247}
248
249impl crate::solvers::BruteForceProblem for MinimumDecisionTree {
250 fn dimensions(&self) -> Vec<usize> {
251 vec![self.num_tests + 1; self.num_tree_slots()]
253 }
254}
255
256crate::declare_variants! {
257 default MinimumDecisionTree => "num_tests^num_objects" create MinimumDecisionTreeCreateSpec,
258}
259
260crate::register_brute_force! {
261 MinimumDecisionTree,
262}
263
264#[cfg(feature = "example-db")]
265pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
266 vec![crate::example_db::specs::ModelExampleSpec {
267 id: "minimum_decision_tree",
268 instance: Box::new(MinimumDecisionTree::new(
269 vec![
270 vec![true, true, false, false],
271 vec![true, false, false, false],
272 vec![false, true, false, true],
273 ],
274 4,
275 3,
276 )),
277 optimal_config: serde_json::json!(vec![0, 2, 1, 3, 3, 3, 3]),
279 optimal_value: serde_json::json!(8),
280 }]
281}
282
283#[cfg(test)]
284#[path = "../../unit_tests/models/misc/minimum_decision_tree.rs"]
285mod tests;