use crate::algorithms::{count_to_f64, euclidean_sq};
use std::cmp::Ordering;
#[must_use]
pub fn divisive(data: &[Vec<f64>], k: usize) -> Vec<usize> {
let n = data.len();
if n == 0 || k == 0 {
return Vec::new();
}
let target = k.min(n);
let mut clusters: Vec<Vec<usize>> = vec![(0..n).collect()];
while clusters.len() < target {
let Some(idx) = select_cluster(&clusters, data) else {
break;
};
if idx >= clusters.len() {
break;
}
let cluster = clusters.swap_remove(idx);
let (old, splinter) = split_cluster(data, &cluster);
if old.is_empty() || splinter.is_empty() {
clusters.push(cluster);
break;
}
clusters.push(old);
clusters.push(splinter);
}
labels_from_clusters(&clusters, n)
}
fn point_distance(data: &[Vec<f64>], i: usize, j: usize) -> f64 {
match (data.get(i), data.get(j)) {
(Some(a), Some(b)) => euclidean_sq(a, b).sqrt(),
_ => f64::INFINITY,
}
}
fn diameter_sq(data: &[Vec<f64>], cluster: &[usize]) -> f64 {
let mut max = 0.0_f64;
for (pos, &i) in cluster.iter().enumerate() {
for &j in cluster.iter().skip(pos + 1) {
let d = match (data.get(i), data.get(j)) {
(Some(a), Some(b)) => euclidean_sq(a, b),
_ => f64::INFINITY,
};
if d > max {
max = d;
}
}
}
max
}
fn select_cluster(clusters: &[Vec<usize>], data: &[Vec<f64>]) -> Option<usize> {
let mut best: Option<usize> = None;
let mut best_diam = f64::NEG_INFINITY;
let mut best_min = usize::MAX;
for (idx, cluster) in clusters.iter().enumerate() {
if cluster.len() < 2 {
continue;
}
let diam = diameter_sq(data, cluster);
let min_member = cluster.iter().copied().min().unwrap_or(usize::MAX);
let ord = diam.total_cmp(&best_diam);
let take = best.is_none()
|| ord == Ordering::Greater
|| (ord == Ordering::Equal && min_member < best_min);
if take {
best = Some(idx);
best_diam = diam;
best_min = min_member;
}
}
best
}
fn split_cluster(data: &[Vec<f64>], cluster: &[usize]) -> (Vec<usize>, Vec<usize>) {
let seed = choose_seed(data, cluster);
let mut old: Vec<usize> = cluster.iter().copied().filter(|&p| p != seed).collect();
let mut splinter = vec![seed];
while old.len() >= 2 {
let Some((mover, delta)) = best_mover(data, &old, &splinter) else {
break;
};
if delta > 0.0 {
old.retain(|&p| p != mover);
splinter.push(mover);
} else {
break;
}
}
old.sort_unstable();
splinter.sort_unstable();
(old, splinter)
}
fn choose_seed(data: &[Vec<f64>], cluster: &[usize]) -> usize {
let mut best_point = cluster.first().copied().unwrap_or(0);
let mut best_avg = f64::NEG_INFINITY;
let denom = count_to_f64(cluster.len().saturating_sub(1));
for &p in cluster {
let sum: f64 = cluster
.iter()
.filter(|&&q| q != p)
.map(|&q| point_distance(data, p, q))
.sum();
let avg = if denom > 0.0 { sum / denom } else { 0.0 };
if avg > best_avg {
best_avg = avg;
best_point = p;
}
}
best_point
}
fn best_mover(data: &[Vec<f64>], old: &[usize], splinter: &[usize]) -> Option<(usize, f64)> {
let rest_denom = count_to_f64(old.len().saturating_sub(1));
let spl_denom = count_to_f64(splinter.len());
if rest_denom <= 0.0 || spl_denom <= 0.0 {
return None;
}
let mut best: Option<(usize, f64)> = None;
for &i in old {
let rest_sum: f64 = old
.iter()
.filter(|&&q| q != i)
.map(|&q| point_distance(data, i, q))
.sum();
let spl_sum: f64 = splinter.iter().map(|&q| point_distance(data, i, q)).sum();
let delta = (rest_sum / rest_denom) - (spl_sum / spl_denom);
let take = match best {
Some((_, bd)) => delta > bd,
None => true,
};
if take {
best = Some((i, delta));
}
}
best
}
fn labels_from_clusters(clusters: &[Vec<usize>], n: usize) -> Vec<usize> {
let mut order: Vec<(usize, usize)> = clusters
.iter()
.enumerate()
.map(|(idx, c)| (c.iter().copied().min().unwrap_or(usize::MAX), idx))
.collect();
order.sort_unstable();
let mut labels = vec![0_usize; n];
for (label, &(_min, idx)) in order.iter().enumerate() {
if let Some(cluster) = clusters.get(idx) {
for &p in cluster {
if let Some(slot) = labels.get_mut(p) {
*slot = label;
}
}
}
}
labels
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn k1_puts_every_point_in_cluster_zero() {
let data = vec![vec![0.0], vec![5.0], vec![9.0]];
let labels = divisive(&data, 1);
assert_eq!(labels, vec![0, 0, 0], "k=1 labels = {labels:?}");
}
#[test]
fn recovers_two_well_separated_blobs() {
let data = vec![
vec![0.0, 0.0],
vec![0.0, 1.0],
vec![1.0, 0.0],
vec![10.0, 10.0],
vec![10.0, 11.0],
vec![11.0, 10.0],
];
let labels = divisive(&data, 2);
assert_eq!(labels, vec![0, 0, 0, 1, 1, 1], "blob labels = {labels:?}");
}
#[test]
fn identical_calls_produce_identical_labels() {
let data = vec![
vec![0.0, 0.0],
vec![0.3, 0.1],
vec![5.0, 5.0],
vec![5.2, 4.9],
vec![9.0, 1.0],
];
let first = divisive(&data, 3);
let second = divisive(&data, 3);
assert_eq!(first, second, "runs diverged: {first:?} vs {second:?}");
}
#[test]
fn k_at_least_n_makes_every_point_a_singleton() {
let data = vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0]];
let labels = divisive(&data, 10);
assert_eq!(labels, vec![0, 1, 2, 3], "k>=n labels = {labels:?}");
}
#[test]
fn empty_input_and_zero_k_yield_empty_labels() {
let empty: Vec<Vec<f64>> = Vec::new();
assert!(
divisive(&empty, 3).is_empty(),
"empty data must give no labels"
);
let data = vec![vec![0.0], vec![1.0]];
assert!(divisive(&data, 0).is_empty(), "k=0 must give no labels");
}
#[test]
fn hand_worked_1d_example_matches_traced_diana_run() {
let data = vec![
vec![1.0],
vec![2.0],
vec![3.0],
vec![10.0],
vec![11.0],
vec![12.0],
vec![25.0],
vec![26.0],
];
let labels = divisive(&data, 3);
assert_eq!(
labels,
vec![0, 0, 0, 1, 1, 1, 2, 2],
"hand-worked labels = {labels:?}"
);
}
#[test]
fn labels_span_zero_to_k_with_no_empty_cluster() {
let data = vec![
vec![0.0, 0.0],
vec![0.4, 0.2],
vec![8.0, 8.0],
vec![8.3, 7.7],
vec![20.0, 1.0],
vec![20.5, 0.8],
];
let k = 3;
let labels = divisive(&data, k);
assert_eq!(labels.len(), data.len(), "one label per point: {labels:?}");
assert!(
labels.iter().all(|&l| l < k),
"labels out of 0..{k}: {labels:?}"
);
let mut counts = vec![0_usize; k];
for &l in &labels {
if let Some(slot) = counts.get_mut(l) {
*slot += 1;
}
}
assert!(
counts.iter().all(|&c| c > 0),
"an empty cluster: {counts:?}"
);
}
}