Disjoint-set forest in Python
Representation of disjoint sets as rooted trees, with a parent pointer per element and a root as representative.
- File
disjoint_set_forest.py, 86 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/disjoint-set-forest/. The authors are listed in prov/B24A25.jsonl.
from collections.abc import Hashable
from typing import Generic, TypeVar
T = TypeVar("T", bound=Hashable)
class DisjointSetForest(Generic[T]):
"""Disjoint sets as a forest with union by rank and path compression.
Each element has a parent and a rank. A root is its own parent and is
the representative of its tree. make_set takes O(1) time; find and union
take O(log n) worst-case time and O(alpha(n)) amortized time, where n is
the number of elements. find needs no recursion: it walks up to the root,
then walks the path again and points each node at the root.
"""
def __init__(self) -> None:
self.parent: dict[T, T] = {}
self.rank: dict[T, int] = {}
self.count = 0
def __contains__(self, x: object) -> bool:
return x in self.parent
def make_set(self, x: T) -> None:
"""Add x as a set of its own. Raise ValueError if x is already present."""
if x in self.parent:
raise ValueError("element already present")
self.parent[x] = x # @line L1
self.rank[x] = 0 # @line L2
self.count += 1
def find(self, x: T) -> T:
"""Return the root of the tree of x, and compress the path to it."""
p = self.parent
root = x
while p[root] != root: # @line L3
root = p[root]
while p[x] != root:
p[x], x = root, p[x] # @line L4
return root # @line L5
def union(self, x: T, y: T) -> bool:
"""Merge the sets of x and y. Return False if they were already one set."""
x, y = self.find(x), self.find(y) # @line L6
if x == y: # @line L7
return False
if self.rank[x] > self.rank[y]: # @line L8
self.parent[y] = x
else:
self.parent[x] = y # @line L9
if self.rank[x] == self.rank[y]: # @line L10
self.rank[y] += 1
self.count -= 1
return True
def connected(self, x: T, y: T) -> bool:
"""Return True if x and y are in the same set."""
return self.find(x) == self.find(y)
def check_invariant(self) -> None:
"""Check that the parent pointers form a forest and that ranks grow strictly upward."""
roots = 0
for x, px in self.parent.items():
if px == x:
roots += 1
else:
assert self.rank[x] < self.rank[px]
assert roots == self.count
n = len(self.parent)
for x in self.parent:
if self.parent[x] == x:
size = sum(1 for y in self.parent if self._root_no_compress(y) == x)
assert size >= 1 << self.rank[x]
assert self.rank[x] <= n.bit_length() - 1
def _root_no_compress(self, x: T) -> T:
steps = 0
while self.parent[x] != x:
x = self.parent[x]
steps += 1
assert steps <= len(self.parent)
return x
test_disjoint_set_forest.py 67 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/disjoint-set-forest/. The authors are listed in prov/B24A25.jsonl.
import random
import pytest
from disjoint_set_forest import DisjointSetForest
def test_example() -> None:
d: DisjointSetForest[int] = DisjointSetForest()
for x in range(8):
d.make_set(x)
for a, b in [(0, 1), (2, 3), (0, 2), (4, 5), (6, 7), (4, 6), (0, 4)]:
assert d.union(a, b)
d.check_invariant()
assert d.parent == {0: 3, 1: 3, 2: 3, 3: 7, 4: 7, 5: 7, 6: 7, 7: 7}
assert d.rank == {0: 0, 1: 1, 2: 0, 3: 2, 4: 0, 5: 1, 6: 0, 7: 3}
assert d.find(0) == 7
assert d.parent[0] == 7 and d.parent[1] == 3
assert d.count == 1
assert not d.union(2, 6)
def test_edges() -> None:
d: DisjointSetForest[str] = DisjointSetForest()
d.make_set("a")
assert d.find("a") == "a"
assert not d.union("a", "a")
assert d.connected("a", "a")
with pytest.raises(ValueError):
d.make_set("a")
with pytest.raises(KeyError):
d.find("b")
def test_long_chain_has_log_height() -> None:
d: DisjointSetForest[int] = DisjointSetForest()
n = 1 << 12
for x in range(n):
d.make_set(x)
for x in range(1, n):
d.union(0, x)
d.check_invariant()
assert max(d.rank.values()) <= 12
def test_random_against_labels() -> None:
rng = random.Random(19)
for _ in range(500):
n = rng.randrange(1, 15)
d: DisjointSetForest[int] = DisjointSetForest()
label = list(range(n))
for x in range(n):
d.make_set(x)
for _ in range(rng.randrange(30)):
a, b = rng.randrange(n), rng.randrange(n)
if rng.random() < 0.5:
merged = d.union(a, b)
assert merged == (label[a] != label[b])
old = label[b]
label = [label[a] if v == old else v for v in label]
else:
assert d.connected(a, b) == (label[a] == label[b])
d.check_invariant()
assert d.count == len(set(label))
What the next level needs
- the tests did not run in this build