codex/kruskal-component-merge-ledger.py

kruskal-component-merge-ledger.py
"""Draw component memberships, not union-find pointers; Python 3.14/matplotlib 3.10.7."""
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def main():
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
    fig, ax = plt.subplots(figsize=(11, 6.5), facecolor="white")
    fig.suptitle("The component ledger: reject a cycle, keep the partition",
                 fontsize=17, fontweight="bold", y=.96)
    rows = [
        ("Start", [["A"], ["B"], ["C"], ["D"], ["E"]], "—", 5, 0),
        ("AB  ·  1", [["A", "B"], ["C"], ["D"], ["E"]], "Different → merge", 4, 1),
        ("BC  ·  2", [["A", "B", "C"], ["D"], ["E"]], "Different → merge", 3, 3),
        ("AC  ·  3", [["A", "B", "C"], ["D"], ["E"]], "Same → reject", 3, 3),
        ("CD  ·  4", [["A", "B", "C", "D"], ["E"]], "Different → merge", 2, 7),
        ("DE  ·  5", [["A", "B", "C", "D", "E"]], "Different → merge", 1, 12),
    ]
    palette = {"A": "#d3eee6", "B": "#e0e8fa", "C": "#fff0ce",
               "D": "#e8dff4", "E": "#e0e9ee"}
    for x, title in [(.1, "Examined edge\n· weight"), (2.05, "Partition after decision"),
                     (5.95, "Endpoint blocks\nbefore decision"), (8.3, "Components"), (9.75, "Cost")]:
        ax.text(x, 6.55, title, fontweight="bold", ha="center" if x >= 8 else "left", va="center")
    for i, (edge, blocks, decision, count, cost) in enumerate(rows):
        y = 5.7 - i * .86
        if i == 3:
            ax.add_patch(FancyBboxPatch((-.1, y - .36), 10.25, .72,
                         boxstyle="round,pad=0.03", fc="#fff0f1", ec="none"))
        ax.text(.1, y, edge, va="center", fontweight="bold", color="#bf3346" if i == 3 else "#23374d")
        for block in blocks:
            start, end = ord(block[0]) - 65, ord(block[-1]) - 65
            x = 2.05 + start * .67
            ax.add_patch(FancyBboxPatch((x, y - .25), (end - start + 1) * .67 - .07, .5,
                         boxstyle="round,pad=0.025", fc=palette[block[0]], ec="#6c8597", lw=1))
            for v in block:
                ax.text(2.05 + (ord(v) - 65) * .67 + .3, y, v,
                        ha="center", va="center", fontweight="bold")
        ax.text(5.95, y, decision, va="center", color="#bf3346" if i == 3 else "#23374d")
        ax.text(8.3, y, str(count), ha="center", va="center")
        ax.text(9.75, y, str(cost), ha="center", va="center", fontweight="bold")
    ax.text(5, -.02, "find(u) = find(v)  ⇔  same block     |     union(u, v) merges different blocks",
            ha="center", fontsize=12, color="#23374d")
    ax.text(5, -.52, "Each acceptance: components −1, accepted edges +1.   Stop at 1 component and 4 edges.",
            ha="center", fontsize=11, color="#23374d")
    ax.set(xlim=(-.2, 10.3), ylim=(-.85, 7.05))
    ax.axis("off")
    fig.subplots_adjust(left=.04, right=.98, top=.85, bottom=.06)
    fig.savefig(Path(__file__).stem + ".png", dpi=140, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()