Blum's disjoint-set structure in Python
Disjoint-set structure with logarithmic worst-case time per single operation.
- File
blum_union_find.py, 129 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/blum-union-find/. The authors are listed in prov/YNSV90.jsonl.
class BlumUnionFind:
"""Blum's k-UF trees: O(log n / log log n) worst-case time per operation.
The elements 0..n-1 are the leaves. In each tree all leaves are at the
same depth, the root has at least two children unless it is a leaf, and
every other inner node has at least k children. find walks from the leaf
to the root; union moves fewer than k children or adds one edge.
"""
def __init__(self, n: int, k: int | None = None) -> None:
if n < 0:
raise ValueError("n must be non-negative")
if k is None:
lg = max(1, (n - 1).bit_length())
lglg = max(1, lg.bit_length())
k = max(2, -(-lg // lglg))
if k < 2:
raise ValueError("k must be at least 2")
self.n = n
self.k = k
self.parent: list[int] = [-1] * n
self.children: list[list[int]] = [[] for _ in range(n)]
self.height: list[int] = [0] * n
self.name: list[int] = list(range(n))
self.count = n
def _root(self, x: int) -> int:
p = self.parent
while p[x] != -1: # @line L1
x = p[x]
return x
def find(self, x: int) -> int:
"""Return the name of the set of x, an element of that set."""
if not 0 <= x < self.n:
raise IndexError("no such element")
return self.name[self._root(x)] # @line L2
def connected(self, x: int, y: int) -> bool:
"""Return True if x and y are in the same set."""
return self._root(x) == self._root(y)
def _new_node(self, h: int) -> int:
self.parent.append(-1)
self.children.append([])
self.height.append(h)
self.name.append(-1)
return len(self.parent) - 1
def _adopt(self, v: int, c: int) -> None:
self.parent[c] = v
self.children[v].append(c)
def _move_children(self, s: int, v: int) -> None:
for c in self.children[s]:
self._adopt(v, c)
self.children[s] = []
def _down(self, r: int, h: int) -> int:
while self.height[r] > h:
r = self.children[r][0]
return r
def union(self, x: int, y: int) -> bool:
"""Merge the sets of x and y. Return False if they were one set."""
r, s = self._root(x), self._root(y) # @line L3
if r == s:
return False
self.count -= 1
k = self.k
if self.height[r] < self.height[s]: # @line L4
r, s = s, r
if self.height[r] == self.height[s]: # @line L5
dr, ds = len(self.children[r]), len(self.children[s])
if self.height[r] == 0 or (dr >= k and ds >= k): # @line L6
t = self._new_node(self.height[r] + 1)
self.name[t] = self.name[r]
self._adopt(t, r)
self._adopt(t, s)
return True
if dr < ds: # @line L7
r, s = s, r
self._move_children(s, r) # @line L8
return True
if self.height[s] == 0 or len(self.children[s]) >= k: # @line L9
self._adopt(self._down(r, self.height[s] + 1), s) # @line L10
else:
self._move_children(s, self._down(r, self.height[s])) # @line L11
return True # @line L12
def check_invariant(self) -> None:
"""Check the shape of every k-UF tree and the parent and child links."""
roots = set()
for x in range(self.n):
d, y = 0, x
while self.parent[y] != -1:
p = self.parent[y]
assert y in self.children[p]
assert self.height[p] == self.height[y] + 1
y = p
d += 1
assert self.height[y] == d # all leaves of a tree at the same depth
roots.add(y)
assert len(roots) == self.count
live = set()
for r in roots:
stack = [r]
leaves = 0
while stack:
v = stack.pop()
live.add(v)
ch = self.children[v]
if self.height[v] == 0:
assert not ch and v < self.n
leaves += 1
elif v == r:
assert len(ch) >= 2
else:
assert len(ch) >= self.k
for c in ch:
assert self.parent[c] == v
stack.extend(ch)
h = self.height[r]
assert h == 0 or leaves >= 2 * self.k ** (h - 1)
assert 0 <= self.name[r] < self.n and self._root(self.name[r]) == r
test_blum_union_find.py 60 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/blum-union-find/. The authors are listed in prov/YNSV90.jsonl.
import random
import pytest
from blum_union_find import BlumUnionFind
def test_example() -> None:
u = BlumUnionFind(9, k=3)
for x, y in [(0, 1), (2, 3), (0, 2), (4, 5), (6, 4), (7, 8), (0, 4), (7, 0)]:
assert u.union(x, y)
u.check_invariant()
a = u.parent[0]
assert sorted(u.children[a]) == [0, 1, 2, 3, 7, 8]
assert sorted(u.children[u.parent[4]]) == [4, 5, 6]
assert u.height[u._root(0)] == 2
assert len({u.find(x) for x in range(9)}) == 1
assert not u.union(3, 6)
def test_edges() -> None:
u = BlumUnionFind(0)
assert u.count == 0
u = BlumUnionFind(1)
assert u.find(0) == 0
with pytest.raises(IndexError):
u.find(1)
with pytest.raises(ValueError):
BlumUnionFind(3, k=1)
def test_height_bound() -> None:
n = 1 << 14
u = BlumUnionFind(n)
for x in range(1, n):
u.union(x, x - 1)
u.check_invariant()
h = u.height[u._root(0)]
assert u.k ** (h - 1) * 2 <= n
def test_random_against_labels() -> None:
rng = random.Random(99)
for _ in range(500):
n = rng.randrange(1, 40)
u = BlumUnionFind(n, k=rng.choice([None, 2, 3, 4]))
label = list(range(n))
for _ in range(2 * n):
x, y = rng.randrange(n), rng.randrange(n)
if rng.random() < 0.6:
assert u.union(x, y) == (label[x] != label[y])
old = label[x]
label = [label[y] if v == old else v for v in label]
else:
assert u.connected(x, y) == (label[x] == label[y])
assert (u.find(x) == u.find(y)) == (label[x] == label[y])
u.check_invariant()
What the next level needs
- the tests did not run in this build