codex/disjoint-set-union-path-compression-rank.py

disjoint-set-union-path-compression-rank.py
"""Draw the original rank/compression trace; write its PNG to the CWD.

Tested with Python 3.14, matplotlib 3.10.7; see pyproject.toml.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch


def main():
    parent = list(range(8))
    rank = [0] * 8

    def find(x):
        root = x
        while parent[root] != root:
            root = parent[root]
        while parent[x] != x:
            following = parent[x]
            parent[x] = root
            x = following
        return root

    def unite(x, y):
        a, b = find(x), find(y)
        if a == b:
            return False
        if rank[a] < rank[b]:
            a, b = b, a
        parent[b] = a
        if rank[a] == rank[b]:
            rank[a] += 1
        return True

    for call in [(2, 1), (4, 3), (2, 4), (6, 5), (2, 6)]:
        assert unite(*call)
    assert not unite(1, 2)
    before = parent[1:]
    assert before == [2, 2, 4, 2, 6, 2, 7]
    ranks = rank[1:]
    assert ranks == [0, 2, 0, 1, 0, 1, 0]
    assert find(3) == 2
    assert parent[1:] == [2, 2, 2, 2, 6, 2, 7]
    assert find(5) == 2
    after = parent[1:]
    assert after == [2, 2, 2, 2, 2, 2, 7] and rank[1:] == ranks

    fig, axes = plt.subplots(1, 2, figsize=(11.4, 5.0), dpi=120, facecolor="white")
    positions = [
        {2: (2.6, 2.6), 1: (0.7, 1.45), 4: (2.6, 1.45), 6: (4.5, 1.45),
         3: (2.6, 0.3), 5: (4.5, 0.3), 7: (5.8, 2.6)},
        {2: (2.6, 2.6), 1: (0.35, 1.05), 3: (1.45, 1.05), 4: (2.6, 1.05),
         5: (3.75, 1.05), 6: (4.85, 1.05), 7: (5.8, 2.6)},
    ]
    for ax, parents, pos, title in zip(axes, [before, after], positions,
                                     ["Before compression queries", "After find(3), then find(5)"]):
        ax.set(xlim=(-0.2, 6.5), ylim=(-0.85, 3.45), aspect="equal")
        ax.axis("off")
        ax.set_title(title, fontsize=13, pad=8)
        for node, par in enumerate(parents, 1):
            if node != par:
                changed = node in (3, 5)
                ax.add_patch(FancyArrowPatch(pos[node], pos[par], arrowstyle="-|>",
                                            mutation_scale=15, shrinkA=15, shrinkB=15,
                                            linewidth=2 if changed else 1.4,
                                            color="#bd4c32" if changed else "#607385"))
        for node, (x, y) in pos.items():
            ax.add_patch(Circle((x, y), 0.21, facecolor="#fde8c8" if node == 7 else "#d6eaf8",
                                edgecolor="#17324d", linewidth=1.3, zorder=3))
            ax.text(x, y, str(node), ha="center", va="center", fontsize=12, zorder=4)
            ax.text(x + 0.23, y + 0.23, f"r={ranks[node - 1]}", fontsize=10, color="#384d60")
        ax.text(2.7, -0.5, "Root 2: height " + ("2" if parents == before else "1") + "; rank 2",
                ha="center", fontsize=12, color="#17324d")
    fig.suptitle("Parent routes shorten; stored ranks do not change", fontsize=16, y=0.97)
    fig.text(0.5, 0.045, "Arrows point to parents. Red marks the two compressed routes. Root 7 stays separate.",
             ha="center", fontsize=11, color="#384d60")
    fig.subplots_adjust(left=0.03, right=0.98, top=0.83, bottom=0.12, wspace=0.08)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", dpi=120)
    plt.close(fig)


if __name__ == "__main__":
    main()