blob: 1e6d84011b7f76e9066e3e34f262b6b3c802be98 [file] [edit]
// Copyright © 2024 Mel Henning
// SPDX-License-Identifier: MIT
use std::collections::{hash_map, HashMap};
use std::hash::{BuildHasher, Hash};
#[derive(Copy, Clone)]
struct Root<X: Copy> {
size: usize,
representative: X,
}
#[derive(Copy, Clone)]
enum Node<X: Copy> {
Child { parent_idx: usize },
Root(Root<X>),
}
struct NodeVec<X: Copy>(Vec<Node<X>>);
impl<X: Copy> NodeVec<X> {
fn new() -> Self {
NodeVec(Vec::new())
}
fn push(&mut self, representative: X) -> usize {
self.0.push(Node::Root(Root {
size: 1,
representative,
}));
self.0.len() - 1
}
fn find_update_root(&mut self, mut idx: usize) -> (usize, Root<X>) {
loop {
match self.0[idx] {
Node::Child { parent_idx } => {
match self.0[parent_idx] {
Node::Child {
parent_idx: grandparent_idx,
} => {
// "Halving" in Tarjan and van Leeuwen
self.0[idx] = Node::Child {
parent_idx: grandparent_idx,
};
idx = grandparent_idx;
}
Node::Root(parent_root) => {
return (parent_idx, parent_root)
}
}
}
Node::Root(root) => return (idx, root),
}
}
}
}
/// Union-find structure
///
/// This implementation follows Tarjan and van Leeuwen - specifically the
/// "link by size" and "halving" variant.
///
/// Robert E. Tarjan and Jan van Leeuwen. 1984. Worst-case Analysis of Set
/// Union Algorithms. J. ACM 31, 2 (April 1984), 245–281.
/// <https://doi.org/10.1145/62.2160>
pub struct UnionFind<X: Copy + Hash + Eq, H: BuildHasher + Default> {
idx_map: HashMap<X, usize, H>,
nodes: NodeVec<X>,
}
impl<X: Copy + Hash + Eq, H: BuildHasher + Default> UnionFind<X, H> {
/// Create a new union-find structure
///
/// At initialization, each possible value is in its own set
pub fn new() -> Self {
UnionFind {
idx_map: Default::default(),
nodes: NodeVec::new(),
}
}
/// Find the representative element for x
pub fn find(&mut self, x: X) -> X {
match self.idx_map.get(&x) {
Some(&idx) => {
let (_, Root { representative, .. }) =
self.nodes.find_update_root(idx);
representative
}
None => x,
}
}
fn map_or_create(&mut self, x: X) -> usize {
*self.idx_map.entry(x).or_insert_with(|| self.nodes.push(x))
}
/// Union the sets containing a and b
///
/// The representative for a will become the representative of
/// the combined set
pub fn union(&mut self, a: X, b: X) {
if a == b {
return;
}
let a_idx = self.map_or_create(a);
let b_idx = self.map_or_create(b);
let (a_root_idx, a_root) = self.nodes.find_update_root(a_idx);
let (b_root_idx, b_root) = self.nodes.find_update_root(b_idx);
if a_root_idx != b_root_idx {
// Keep the tree balanced
let (new_root_idx, new_child_idx) = if a_root.size >= b_root.size {
(a_root_idx, b_root_idx)
} else {
(b_root_idx, a_root_idx)
};
self.nodes.0[new_root_idx] = Node::Root(Root {
size: a_root.size + b_root.size,
representative: a_root.representative,
});
self.nodes.0[new_child_idx] = Node::Child {
parent_idx: new_root_idx,
};
}
}
/// Return true if find() is the identity mapping
pub fn is_empty(&self) -> bool {
self.nodes.0.is_empty()
}
}
pub struct IntoIter<X: Copy> {
idx_map: hash_map::IntoIter<X, usize>,
nodes: NodeVec<X>,
}
impl<X: Copy> Iterator for IntoIter<X> {
type Item = (X, X);
fn next(&mut self) -> Option<Self::Item> {
let (k, idx) = self.idx_map.next()?;
let (_, root) = self.nodes.find_update_root(idx);
Some((k, root.representative))
}
}
impl<X: Copy + Hash + Eq, H: BuildHasher + Default> IntoIterator
for UnionFind<X, H>
{
type Item = (X, X);
type IntoIter = IntoIter<X>;
fn into_iter(self) -> IntoIter<X> {
IntoIter {
idx_map: self.idx_map.into_iter(),
nodes: self.nodes,
}
}
}
#[cfg(test)]
mod tests {
use crate::union_find::Node;
use crate::union_find::UnionFind;
use std::cmp::max;
use std::hash::BuildHasher;
use std::hash::Hash;
use std::hash::RandomState;
fn ceil_log2(x: usize) -> u32 {
assert!(x > 0);
usize::BITS - (x - 1).leading_zeros()
}
struct HeightInfo {
height: u32,
size: usize,
}
pub struct HeightCalc<'a, X: Copy + Hash + Eq, H: BuildHasher + Default> {
uf: &'a UnionFind<X, H>,
downward_edges: Vec<Vec<usize>>,
}
impl<'a, X: Copy + Hash + Eq, H: BuildHasher + Default> HeightCalc<'a, X, H> {
fn new(uf: &'a UnionFind<X, H>) -> Self {
let mut downward_edges: Vec<Vec<usize>> =
uf.nodes.0.iter().map(|_| Vec::new()).collect();
for (i, node) in uf.nodes.0.iter().enumerate() {
if let Node::Child { parent_idx } = node {
downward_edges[*parent_idx].push(i);
}
}
HeightCalc { uf, downward_edges }
}
fn calc_info(&self, idx: usize) -> HeightInfo {
let mut result = HeightInfo { height: 0, size: 1 };
for child in &self.downward_edges[idx] {
let child_result = self.calc_info(*child);
result.height = max(result.height, child_result.height + 1);
result.size += child_result.size;
}
result
}
fn check_roots(&self) -> u32 {
let mut total_size = 0;
let mut max_height = 0;
for (i, node) in self.uf.nodes.0.iter().enumerate() {
if let Node::Root(root) = node {
let info = self.calc_info(i);
assert_eq!(root.size, info.size);
total_size += info.size;
max_height = max(max_height, info.height);
let max_expected_height = ceil_log2(root.size + 1) - 1;
if info.height > max_expected_height {
eprintln!(
"height {}\t max_expected_height {}\t size {}",
info.height, max_expected_height, info.size
);
}
assert!(info.height <= max_expected_height);
}
}
assert_eq!(total_size, self.uf.nodes.0.len());
assert_eq!(total_size, self.uf.idx_map.len());
max_height
}
pub fn check(uf: &'a UnionFind<X, H>) -> u32 {
HeightCalc::new(uf).check_roots()
}
}
#[test]
fn test_basic() {
let mut f = UnionFind::<u32, RandomState>::new();
assert_eq!(f.find(10), 10);
assert_eq!(f.find(12), 12);
f.union(10, 12);
f.union(11, 13);
HeightCalc::check(&f);
assert_eq!(f.find(13), 11);
assert_eq!(f.find(12), 10);
assert_eq!(f.find(11), 11);
assert_eq!(f.find(10), 10);
f.union(12, 13);
HeightCalc::check(&f);
assert_eq!(f.find(13), 10);
assert_eq!(f.find(12), 10);
assert_eq!(f.find(11), 10);
assert_eq!(f.find(10), 10);
assert_eq!(f.find(14), 14);
HeightCalc::check(&f);
// Union the set with itself
f.union(11, 10);
HeightCalc::check(&f);
assert_eq!(f.find(13), 10);
assert_eq!(f.find(12), 10);
assert_eq!(f.find(11), 10);
assert_eq!(f.find(10), 10);
}
#[test]
fn test_chain_a_height() {
let mut f = UnionFind::<u32, RandomState>::new();
for i in 0..1000 {
f.union(i, i + 1);
HeightCalc::check(&f);
}
assert_eq!(f.find(1000), 0);
}
#[test]
fn test_chain_b_height() {
let mut f = UnionFind::<u32, RandomState>::new();
for i in 0..1000 {
f.union(i + 1, i);
HeightCalc::check(&f);
}
assert_eq!(f.find(0), 1000);
}
#[test]
fn test_binary_tree_height() {
let height = 8;
let count = 1 << height;
let mut f = UnionFind::<usize, RandomState>::new();
for current_height in 0..height {
let stride = 1 << current_height;
for i in (0..count).step_by(2 * stride) {
f.union(i, i + stride);
}
let actual_height = HeightCalc::check(&f);
// actual_height can vary based on tiebreaker condition
assert!(
actual_height == current_height
|| actual_height == current_height + 1
);
}
// Check path halving
let actual_height_before = HeightCalc::check(&f);
for i in 0..count {
assert_eq!(f.find(i), 0);
}
let actual_height_after = HeightCalc::check(&f);
assert!(actual_height_after <= actual_height_before.div_ceil(2));
}
}