Skip to main content

problemreductions/models/misc/
clustering.rs

1//! Clustering problem implementation.
2//!
3//! Given a distance matrix over n elements, a cluster count bound K,
4//! and a diameter bound B, determine whether the elements can be partitioned
5//! into at most K non-empty clusters such that all intra-cluster pairwise
6//! distances are at most B.
7
8use 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/// The Clustering problem.
30///
31/// Given a set of `n` elements with pairwise distances, a cluster count
32/// bound `K`, and a diameter bound `B`, determine whether there exists
33/// a partition of the elements into at most `K` non-empty clusters such
34/// that for every cluster, all pairwise distances within that cluster
35/// are at most `B`.
36///
37/// # Representation
38///
39/// Each element `i` is assigned a cluster index `config[i] ∈ {0, ..., K-1}`.
40/// The problem is satisfiable iff every non-empty cluster has all pairwise
41/// distances ≤ B.
42///
43/// # Example
44///
45/// ```
46/// use problemreductions::models::misc::Clustering;
47/// use problemreductions::{Problem, BruteForce};
48///
49/// // 4 elements, 2 clusters, diameter bound 1
50/// let distances = vec![
51///     vec![0, 1, 3, 3],
52///     vec![1, 0, 3, 3],
53///     vec![3, 3, 0, 1],
54///     vec![3, 3, 1, 0],
55/// ];
56/// let problem = Clustering::new(distances, 2, 1);
57/// let solver = BruteForce::new();
58/// let solution = solver.solve(&problem).unwrap();
59/// assert!(solution.is_some());
60/// ```
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Clustering {
63    /// Symmetric distance matrix with zero diagonal.
64    distances: Vec<Vec<i64>>,
65    /// Maximum number of clusters K.
66    num_clusters: usize,
67    /// Maximum allowed intra-cluster pairwise distance B.
68    diameter_bound: i64,
69}
70
71impl Clustering {
72    /// Create a new Clustering instance.
73    ///
74    /// # Panics
75    ///
76    /// Panics if:
77    /// - `distances` is empty
78    /// - `distances` is not square
79    /// - `distances` is not symmetric
80    /// - diagonal entries are not zero
81    /// - `num_clusters` is zero
82    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    /// Returns the distance matrix.
115    pub fn distances(&self) -> &[Vec<i64>] {
116        &self.distances
117    }
118
119    /// Returns the number of elements.
120    pub fn num_elements(&self) -> usize {
121        self.distances.len()
122    }
123
124    /// Returns the maximum number of clusters K.
125    pub fn num_clusters(&self) -> usize {
126        self.num_clusters
127    }
128
129    /// Returns the diameter bound B.
130    pub fn diameter_bound(&self) -> i64 {
131        self.diameter_bound
132    }
133
134    /// Check if a configuration is a valid clustering.
135    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        // Group elements by cluster in a single pass
144        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        // Check all intra-cluster pairwise distances ≤ B
149        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    // 6 elements in two tight groups {0,1,2} and {3,4,5}
211    // Intra-group distance = 1, inter-group distance = 3
212    // K=2, B=1
213    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;