use std::collections::{BinaryHeap, HashMap};
pub struct Group {
pub representative: usize,
pub members: Vec<usize>,
}
pub fn find(n: usize, edges: &[(usize, usize)]) -> Vec<Group> {
let mut adj: Vec<Vec<u32>> = vec![Vec::new(); n];
for &(a, b) in edges {
if a != b {
adj[a].push(b as u32);
adj[b].push(a as u32);
}
}
for a in adj.iter_mut() {
a.sort_unstable();
a.dedup();
}
let mut ungrouped = vec![false; n];
let mut left = 0usize;
for v in 0..n {
if !adj[v].is_empty() {
ungrouped[v] = true;
left += 1;
}
}
let reach = |v: usize, ungrouped: &[bool]| -> usize {
adj[v].iter().filter(|&&u| ungrouped[u as usize]).count() + ungrouped[v] as usize
};
let mut heap: BinaryHeap<(usize, std::cmp::Reverse<usize>)> = (0..n)
.filter(|&v| ungrouped[v])
.map(|v| (adj[v].len() + 1, std::cmp::Reverse(v)))
.collect();
let mut out: Vec<Group> = Vec::new();
while left > 0 {
let Some((claimed, std::cmp::Reverse(r))) = heap.pop() else { break };
let real = reach(r, &ungrouped);
if real < claimed {
if real > 0 {
heap.push((real, std::cmp::Reverse(r)));
}
continue;
}
let mut members: Vec<usize> = adj[r].iter().map(|&u| u as usize).collect();
members.push(r);
members.sort_unstable();
for &f in members.iter() {
if ungrouped[f] {
ungrouped[f] = false;
left -= 1;
}
}
if members.len() > 1 {
out.push(Group { representative: r, members });
}
}
out
}
pub fn membership(groups: &[Group]) -> HashMap<usize, Vec<usize>> {
let mut out: HashMap<usize, Vec<usize>> = HashMap::new();
for (gi, g) in groups.iter().enumerate() {
for &f in g.members.iter() {
out.entry(f).or_default().push(gi);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn groups_of(n: usize, edges: &[(usize, usize)]) -> Vec<(usize, Vec<usize>)> {
let mut g: Vec<(usize, Vec<usize>)> =
find(n, edges).into_iter().map(|x| (x.representative, x.members)).collect();
g.sort();
g
}
#[test]
fn every_member_matched_the_representative() {
let edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 2), (5, 6), (6, 7)];
let present: std::collections::HashSet<(usize, usize)> =
edges.iter().map(|&(a, b)| (a.min(b), a.max(b))).collect();
for g in find(8, &edges) {
for &m in g.members.iter() {
if m == g.representative {
continue;
}
let key = (m.min(g.representative), m.max(g.representative));
assert!(present.contains(&key), "{m} was never matched against {}", g.representative);
}
}
}
#[test]
fn a_chain_groups_around_its_middle() {
assert_eq!(groups_of(3, &[(0, 1), (1, 2)]), vec![(1, vec![0, 1, 2])]);
}
#[test]
fn a_file_matched_by_two_others_represents_them() {
assert_eq!(groups_of(3, &[(0, 1), (0, 2)]), vec![(0, vec![0, 1, 2])]);
}
#[test]
fn a_second_representative_covers_what_the_first_missed() {
let g = groups_of(5, &[(0, 1), (0, 2), (0, 3), (3, 4)]);
assert_eq!(g, vec![(0, vec![0, 1, 2, 3]), (3, vec![0, 3, 4])]);
assert_eq!(membership(&find(5, &[(0, 1), (0, 2), (0, 3), (3, 4)]))[&3], vec![0, 1]);
}
#[test]
fn every_matched_file_lands_in_a_group() {
let edges = [(0, 1), (1, 2), (3, 4), (5, 6), (6, 7), (7, 8), (8, 5)];
let covered: std::collections::HashSet<usize> =
find(9, &edges).iter().flat_map(|g| g.members.iter().copied()).collect();
for v in 0..9 {
assert!(covered.contains(&v), "{v} matched something and was dropped");
}
}
#[test]
fn unmatched_files_are_in_no_group() {
assert!(find(5, &[]).is_empty());
assert_eq!(groups_of(5, &[(3, 4)]), vec![(3, vec![3, 4])]);
}
#[test]
fn a_repeated_edge_does_not_duplicate_a_member() {
assert_eq!(groups_of(2, &[(0, 1), (1, 0), (0, 1)]), vec![(0, vec![0, 1])]);
}
#[test]
fn a_complete_component_is_one_group() {
let n = 400;
let edges: Vec<(usize, usize)> = (0..n).flat_map(|i| (i + 1..n).map(move |j| (i, j))).collect();
let g = find(n, &edges);
assert_eq!(g.len(), 1);
assert_eq!(g[0].members.len(), n);
assert_eq!(g[0].representative, 0);
}
#[test]
fn a_dense_component_costs_nothing() {
let n = 200;
let edges: Vec<(usize, usize)> = (0..n)
.flat_map(|i| (i + 1..n).map(move |j| (i, j)))
.filter(|&(i, j)| !(i % 2 == 0 && j == i + 1))
.collect();
let g = find(n, &edges);
assert!(g.len() <= 3, "{} groups", g.len());
let covered: std::collections::HashSet<usize> =
g.iter().flat_map(|x| x.members.iter().copied()).collect();
assert_eq!(covered.len(), n);
}
#[test]
fn output_does_not_depend_on_edge_order() {
let edges = [(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (2, 4), (4, 5)];
let mut shuffled: Vec<(usize, usize)> = edges.iter().rev().map(|&(a, b)| (b, a)).collect();
assert_eq!(groups_of(6, &edges), groups_of(6, &shuffled));
shuffled.rotate_left(3);
assert_eq!(groups_of(6, &edges), groups_of(6, &shuffled));
}
}