use {
crate::hash::map,
ahash::HashMap,
core::{hash::Hash, mem, num::NonZero, ops::Deref},
};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
pub(crate) struct RootElement<Element>(Element);
#[non_exhaustive]
#[derive(Clone, Debug)]
pub(crate) struct Root<Element> {
pub(crate) cardinality: NonZero<usize>,
pub(crate) element: RootElement<Element>,
}
#[non_exhaustive]
pub(crate) struct UnionFind<Element> {
upward: HashMap<Element, Upward<Element>>,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, Ord, Hash, PartialEq, PartialOrd)]
enum Upward<Element> {
Parent {
parent: Element,
},
Root {
cardinality: NonZero<usize>,
},
}
impl<Element: PartialEq> PartialEq for Root<Element> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.element == other.element
}
}
impl<Element: Eq> Eq for Root<Element> {}
impl<Element> Deref for RootElement<Element> {
type Target = Element;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Element> Default for UnionFind<Element> {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl<Element> UnionFind<Element> {
#[inline]
#[must_use]
pub(crate) const fn new() -> Self {
Self { upward: map() }
}
}
#[expect(
clippy::expect_used,
clippy::panic,
clippy::unwrap_in_result,
reason = "Internal invariants: violations should fail loudly."
)]
impl<Element> UnionFind<Element>
where
Element: Copy + Eq + Hash,
{
#[inline]
pub(crate) fn insert_singleton(&mut self, element: Element) {
if self
.upward
.insert(
element,
Upward::Root {
cardinality: const { NonZero::new(1).unwrap() },
},
)
.is_some()
{
panic!("INTERNAL ERROR (`pbt`): attempting to insert an already-present singleton")
}
}
#[inline]
pub(crate) fn merge(&mut self, lhs: Element, rhs: Element) {
let mut lhs_root = self
.root(lhs)
.expect("INTERNAL ERROR (`pbt`): merging unregistered Union-Find entry");
let mut rhs_root = self
.root(rhs)
.expect("INTERNAL ERROR (`pbt`): merging unregistered Union-Find entry");
if lhs_root.element == rhs_root.element {
return;
}
if rhs_root.cardinality > lhs_root.cardinality {
let () = mem::swap(&mut lhs_root, &mut rhs_root);
}
let rhs_update = self
.upward
.get_mut(&rhs_root.element)
.expect("INTERNAL ERROR (`pbt`): Union-Find entry erased");
*rhs_update = Upward::Parent {
parent: *lhs_root.element,
};
let lhs_update = self
.upward
.get_mut(&lhs_root.element)
.expect("INTERNAL ERROR (`pbt`): Union-Find entry erased");
*lhs_update = Upward::Root {
#[expect(clippy::multiple_unsafe_ops_per_block, reason = "logically connected")]
cardinality: unsafe {
NonZero::new_unchecked(
lhs_root
.cardinality
.get()
.unchecked_add(rhs_root.cardinality.get()),
)
},
};
}
#[inline]
pub(crate) fn root(&mut self, element: Element) -> Option<Root<Element>> {
Some(match *self.upward.get(&element)? {
Upward::Root { cardinality } => Root {
element: RootElement(element),
cardinality,
},
Upward::Parent { parent } => {
let root = self
.root(parent)
.expect("INTERNAL ERROR (`pbt`): Union-Find parent is unregistered");
let shorten = self
.upward
.get_mut(&element)
.expect("INTERNAL ERROR (`pbt`): Union-Find entry erased");
*shorten = Upward::Parent {
parent: *root.element,
};
root
}
})
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::unwrap_used, reason = "Failing tests ought to panic.")]
use {super::*, pretty_assertions::assert_eq};
#[test]
#[expect(
clippy::cognitive_complexity,
reason = "[don draper voice] that's what the comments are for!"
)]
fn union_find_12345() {
const SINGLETON: Upward<u8> = Upward::Root {
cardinality: NonZero::new(1).unwrap(),
};
let mut uf = UnionFind::new();
for i in 1..=5_u8 {
uf.insert_singleton(i);
}
assert_eq!(uf.upward.get(&1), Some(&SINGLETON));
assert_eq!(uf.upward.get(&2), Some(&SINGLETON));
assert_eq!(uf.upward.get(&3), Some(&SINGLETON));
assert_eq!(uf.upward.get(&4), Some(&SINGLETON));
assert_eq!(uf.upward.get(&5), Some(&SINGLETON));
uf.merge(2, 3);
assert_eq!(uf.upward.get(&1), Some(&SINGLETON));
assert_eq!(
uf.upward.get(&2),
Some(&Upward::Root {
cardinality: NonZero::new(2).unwrap(),
})
);
assert_eq!(uf.upward.get(&3), Some(&Upward::Parent { parent: 2 }));
assert_eq!(uf.upward.get(&4), Some(&SINGLETON));
assert_eq!(uf.upward.get(&5), Some(&SINGLETON));
uf.merge(4, 5);
assert_eq!(uf.upward.get(&1), Some(&SINGLETON));
assert_eq!(
uf.upward.get(&2),
Some(&Upward::Root {
cardinality: NonZero::new(2).unwrap(),
})
);
assert_eq!(uf.upward.get(&3), Some(&Upward::Parent { parent: 2 }));
assert_eq!(
uf.upward.get(&4),
Some(&Upward::Root {
cardinality: NonZero::new(2).unwrap(),
})
);
assert_eq!(uf.upward.get(&5), Some(&Upward::Parent { parent: 4 }));
uf.merge(3, 5);
assert_eq!(uf.upward.get(&1), Some(&SINGLETON));
assert_eq!(
uf.upward.get(&2),
Some(&Upward::Root {
cardinality: NonZero::new(4).unwrap(),
})
);
assert_eq!(uf.upward.get(&3), Some(&Upward::Parent { parent: 2 }));
assert_eq!(uf.upward.get(&4), Some(&Upward::Parent { parent: 2 }));
assert_eq!(uf.upward.get(&5), Some(&Upward::Parent { parent: 4 }));
assert_eq!(
uf.root(5),
Some(Root {
cardinality: NonZero::new(4).unwrap(),
element: RootElement(2),
})
);
assert_eq!(uf.upward.get(&1), Some(&SINGLETON));
assert_eq!(
uf.upward.get(&2),
Some(&Upward::Root {
cardinality: NonZero::new(4).unwrap(),
})
);
assert_eq!(uf.upward.get(&3), Some(&Upward::Parent { parent: 2 }));
assert_eq!(uf.upward.get(&4), Some(&Upward::Parent { parent: 2 }));
assert_eq!(uf.upward.get(&5), Some(&Upward::Parent { parent: 2 })); }
#[test]
fn merge_keeps_larger_set_as_root() {
let mut uf = UnionFind::<usize>::new();
uf.insert_singleton(1);
uf.insert_singleton(2);
uf.insert_singleton(3);
uf.merge(1, 2);
uf.merge(3, 1);
assert_eq!(uf.root(1).unwrap().element, RootElement(1));
assert_eq!(uf.root(2).unwrap().element, RootElement(1));
assert_eq!(uf.root(3).unwrap().element, RootElement(1));
assert_eq!(uf.root(1).unwrap().cardinality.get(), 3);
}
}