problemreductions/models/misc/
clustering.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::Problem;
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13 ProblemSchemaEntry {
14 name: "Clustering",
15 display_name: "Clustering",
16 aliases: &[],
17 dimensions: &[],
18 category: crate::registry::ProblemCategory::Misc,
19 module_path: module_path!(),
20 description: "Partition elements into at most K clusters where all intra-cluster distances are at most B",
21 fields: &[
22 FieldInfo { name: "distances", type_name: "Vec<Vec<i64>>", description: "Symmetric distance matrix with zero diagonal" },
23 FieldInfo { name: "num_clusters", type_name: "usize", description: "Maximum number of clusters K" },
24 FieldInfo { name: "diameter_bound", type_name: "i64", description: "Maximum allowed intra-cluster pairwise distance B" },
25 ],
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Clustering {
63 distances: Vec<Vec<i64>>,
65 num_clusters: usize,
67 diameter_bound: i64,
69}
70
71impl Clustering {
72 pub fn new(distances: Vec<Vec<i64>>, num_clusters: usize, diameter_bound: i64) -> Self {
83 let n = distances.len();
84 assert!(n > 0, "Clustering requires at least one element");
85 assert!(num_clusters > 0, "num_clusters must be at least 1");
86 for (i, row) in distances.iter().enumerate() {
87 assert_eq!(
88 row.len(),
89 n,
90 "Distance matrix must be square: row {i} has {} columns, expected {n}",
91 row.len()
92 );
93 assert_eq!(
94 distances[i][i], 0,
95 "Diagonal entry distances[{i}][{i}] must be 0"
96 );
97 }
98 for (i, row_i) in distances.iter().enumerate() {
99 for j in (i + 1)..n {
100 assert_eq!(
101 row_i[j], distances[j][i],
102 "Distance matrix must be symmetric: distances[{i}][{j}] = {} != distances[{j}][{i}] = {}",
103 row_i[j], distances[j][i]
104 );
105 }
106 }
107 Self {
108 distances,
109 num_clusters,
110 diameter_bound,
111 }
112 }
113
114 pub fn distances(&self) -> &[Vec<i64>] {
116 &self.distances
117 }
118
119 pub fn num_elements(&self) -> usize {
121 self.distances.len()
122 }
123
124 pub fn num_clusters(&self) -> usize {
126 self.num_clusters
127 }
128
129 pub fn diameter_bound(&self) -> i64 {
131 self.diameter_bound
132 }
133
134 fn is_valid_partition(&self, config: &[usize]) -> bool {
136 let n = self.num_elements();
137 if config.len() != n {
138 return false;
139 }
140 if config.iter().any(|&c| c >= self.num_clusters) {
141 return false;
142 }
143 let mut clusters: Vec<Vec<usize>> = vec![vec![]; self.num_clusters];
145 for (i, &c) in config.iter().enumerate() {
146 clusters[c].push(i);
147 }
148 for members in &clusters {
150 for a in 0..members.len() {
151 for b in (a + 1)..members.len() {
152 if self.distances[members[a]][members[b]] > self.diameter_bound {
153 return false;
154 }
155 }
156 }
157 }
158 true
159 }
160}
161
162impl Problem for Clustering {
163 const NAME: &'static str = "Clustering";
164 type Solution = Vec<usize>;
165 type Value = crate::types::Or;
166
167 crate::problem_parameters![
168 ("num_clusters", num_clusters),
169 ("num_elements", num_elements),
170 ];
171
172 fn variant() -> Vec<(&'static str, &'static str)> {
173 crate::variant_params![]
174 }
175
176 fn evaluate(
177 &self,
178 config: &Self::Solution,
179 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
180 if config.len() != self.num_elements() {
181 return Err(crate::traits::EvaluationError::InvalidConfiguration(
182 "cluster assignment length does not match the elements".into(),
183 ));
184 }
185 if config.iter().any(|&cluster| cluster >= self.num_clusters) {
186 return Err(crate::traits::EvaluationError::InvalidConfiguration(
187 "cluster assignment contains an out-of-range cluster".into(),
188 ));
189 }
190 Ok(crate::types::Or(self.is_valid_partition(config)))
191 }
192}
193
194impl crate::solvers::BruteForceProblem for Clustering {
195 fn dimensions(&self) -> Vec<usize> {
196 vec![self.num_clusters; self.num_elements()]
197 }
198}
199
200crate::declare_variants! {
201 default Clustering => "num_clusters^num_elements",
202}
203
204crate::register_brute_force! {
205 Clustering,
206}
207
208#[cfg(feature = "example-db")]
209pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
210 let distances = vec![
214 vec![0, 1, 1, 3, 3, 3],
215 vec![1, 0, 1, 3, 3, 3],
216 vec![1, 1, 0, 3, 3, 3],
217 vec![3, 3, 3, 0, 1, 1],
218 vec![3, 3, 3, 1, 0, 1],
219 vec![3, 3, 3, 1, 1, 0],
220 ];
221 vec![crate::example_db::specs::ModelExampleSpec {
222 id: "clustering",
223 instance: Box::new(Clustering::new(distances, 2, 1)),
224 optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1]),
225 optimal_value: serde_json::json!(true),
226 }]
227}
228
229#[cfg(test)]
230#[path = "../../unit_tests/models/misc/clustering.rs"]
231mod tests;