유니온 파인드 문서의 구현 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""15.유니온 파인드: 크기로 합치기와 경로 압축을 쓴 서로소 집합과 문서 주장의 확인."""
import math
import random
from collections import deque


class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

    def find(self, x):
        root = x
        while self.parent[root] != root:          # 대표까지 올라간다
            root = self.parent[root]
        while self.parent[x] != root:             # 지나온 칸을 모두 대표에 바로 잇는다(경로 압축)
            self.parent[x], x = root, self.parent[x]
        return root

    def union(self, a, b):
        a, b = self.find(a), self.find(b)
        if a == b:
            return False
        if self.size[a] < self.size[b]:           # 작은 무리를 큰 무리 밑에
            a, b = b, a
        self.parent[b] = a
        self.size[a] += self.size[b]
        return True


def depth_without_compression(n, pairs, by_size):
    parent, size = list(range(n)), [1] * n

    def root(x):
        d = 0
        while parent[x] != x:
            x = parent[x]
            d += 1
        return x, d

    for a, b in pairs:
        ra, rb = root(a)[0], root(b)[0]
        if ra == rb:
            continue
        if by_size and size[ra] < size[rb]:
            ra, rb = rb, ra
        parent[rb] = ra
        size[ra] += size[rb]
    return max(root(x)[1] for x in range(n))


def same_by_bfs(n, pairs, a, b):
    g = [[] for _ in range(n)]
    for x, y in pairs:
        g[x].append(y)
        g[y].append(x)
    seen, q = {a}, deque([a])
    while q:
        v = q.popleft()
        for u in g[v]:
            if u not in seen:
                seen.add(u)
                q.append(u)
    return b in seen


if __name__ == "__main__":
    # 예시로 보기: 0 ~ 5, union(0,1), union(2,3), union(1,3), union(4,5)
    d = DSU(6)
    d.union(0, 1)
    assert d.parent == [0, 0, 2, 3, 4, 5]
    d.union(2, 3)
    assert d.parent == [0, 0, 2, 2, 4, 5]
    d.union(1, 3)
    assert d.parent == [0, 0, 0, 2, 4, 5] and d.size[0] == 4
    d.union(4, 5)
    assert d.parent == [0, 0, 0, 2, 4, 4]
    assert d.find(3) == 0 and d.parent[3] == 0                 # 찾으면서 3을 0에 바로 잇는다
    assert d.find(5) != d.find(0)
    # C1: 0 ~ 4, union(3,4), union(1,2), union(2,4), 대표 확인
    c = DSU(5)
    for a, b in ((3, 4), (1, 2), (2, 4)):
        c.union(a, b)
    assert c.parent == [0, 1, 1, 1, 3]
    groups = {}
    for x in range(5):
        groups.setdefault(c.find(x), []).append(x)
    assert sorted(groups.values()) == [[0], [1, 2, 3, 4]]
    # C3: 크기를 보지 않고 늘 두 번째를 첫 번째 밑에 두면 한 줄이 된다
    chain = [(i + 1, i) for i in range(9)]          # union(1,0), union(2,1), … : 지금까지의 무리가 늘 새 칸 밑으로
    assert depth_without_compression(10, chain, by_size=False) == 9
    assert depth_without_compression(10, chain, by_size=True) == 1
    # 크기로 합치면 깊이 ≤ log2 n (경로 압축 없이도)
    rng = random.Random(15)
    for _ in range(500):
        n = rng.randint(1, 64)
        pairs = [(rng.randrange(n), rng.randrange(n)) for _ in range(rng.randint(0, 3 * n))]
        assert depth_without_compression(n, pairs, by_size=True) <= math.log2(n) if n > 1 else True
    # 무작위: 같은 무리 판정 = BFS
    for _ in range(1500):
        n = rng.randint(1, 12)
        pairs = [(rng.randrange(n), rng.randrange(n)) for _ in range(rng.randint(0, 12))]
        ds = DSU(n)
        for a, b in pairs:
            ds.union(a, b)
        for _ in range(8):
            a, b = rng.randrange(n), rng.randrange(n)
            assert (ds.find(a) == ds.find(b)) == same_by_bfs(n, pairs, a, b)
    print("ALL CHECKS PASSED")