codexpruefer-tree-encoding.py
pruefer-tree-encoding.py
"""Smallest-leaf Prüfer encoding and its inverse; tested with Python 3.14.
Write the matching opaque PNG into the current working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
def encoding_steps(edges, labels):
adjacency = {v: set() for v in labels}
for a, b in edges:
adjacency[a].add(b)
adjacency[b].add(a)
steps, code = [], []
while len(adjacency) > 2:
leaf = min(v for v, neighbours in adjacency.items() if len(neighbours) == 1)
neighbour = next(iter(adjacency[leaf]))
current = [(a, b) for a in adjacency for b in adjacency[a] if a < b]
steps.append((set(adjacency), current, leaf, neighbour))
code.append(neighbour)
adjacency[neighbour].remove(leaf)
del adjacency[leaf]
steps.append((set(adjacency), [tuple(sorted(adjacency))], None, None))
return steps, tuple(code)
def main():
edges = [(1, 2), (1, 3), (3, 4), (3, 5), (5, 6)]
steps, code = encoding_steps(edges, range(1, 7))
assert code == (1, 3, 3, 5)
positions = {1: (-0.7, 0.4), 2: (-1.2, 1.35), 3: (0, -0.1),
4: (-0.65, -1.05), 5: (0.9, 0.4), 6: (1.4, 1.35)}
blue, orange, dark = "#226b96", "#b55c1c", "#243746"
fig = plt.figure(figsize=(12, 6.2), dpi=120, facecolor="white")
grid = fig.add_gridspec(2, 5, height_ratios=(1.1, 1.15))
for i, (remaining, current, leaf, neighbour) in enumerate(steps):
ax = fig.add_subplot(grid[0, i])
for a, b in current:
active = leaf in (a, b)
ax.plot([positions[a][0], positions[b][0]],
[positions[a][1], positions[b][1]],
color=orange if active else blue, lw=2.5, zorder=1)
for v in sorted(remaining):
active = v == leaf
ax.scatter(*positions[v], s=460, facecolor="#fff0e3" if active else "white",
edgecolor=orange if active else blue, lw=1.8, zorder=2)
ax.text(*positions[v], str(v), ha="center", va="center", fontsize=12,
color=dark, zorder=3)
ax.set_xlim(-1.7, 1.9)
ax.set_ylim(-1.65, 1.9)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(f"Step {i + 1}" if leaf is not None else "Final pair", fontsize=12)
caption = f"Remove {leaf}; record {neighbour}" if leaf is not None else "Stop; keep edge {5, 6}"
ax.text(0.5, -0.08, caption, transform=ax.transAxes, ha="center", fontsize=11,
color=orange if leaf is not None else blue)
ax = fig.add_subplot(grid[1, :])
ax.axis("off")
ax.text(0.5, 1.04, "Decode (1, 3, 3, 5): choose the smallest remaining label absent from the code",
ha="center", transform=ax.transAxes, fontsize=12, color=dark)
remaining, pending, recovered, rows = set(range(1, 7)), list(code), [], []
while pending:
leaf = min(remaining - set(pending))
neighbour = pending[0]
rows.append([str(tuple(pending)), str(leaf), f"{{{leaf}, {neighbour}}}"])
recovered.append(tuple(sorted((leaf, neighbour))))
remaining.remove(leaf)
pending.pop(0)
a, b = sorted(remaining)
recovered.append((a, b))
rows.append(["Empty; remaining labels {5, 6}", "Final pair", "{5, 6}"])
assert set(recovered) == set(edges)
table = ax.table(cellText=rows,
colLabels=["Remaining code", "Smallest absent label", "Edge added"],
colWidths=[0.48, 0.28, 0.24], cellLoc="center", bbox=[0.08, 0.05, 0.84, 0.87])
table.auto_set_font_size(False)
table.set_fontsize(11)
for (row, _), cell in table.get_celld().items():
cell.set_edgecolor("#d2dce3")
cell.set_facecolor("#eaf3f8" if row == 0 else "white")
cell.set_text_props(color=dark, weight="bold" if row == 0 else "normal")
fig.suptitle("A tree becomes four freely chosen labels", fontsize=17, y=0.97, color=dark)
fig.text(0.5, 0.915, "Orange marks the smallest leaf and the edge removed; node positions stay fixed.",
ha="center", fontsize=11, color=dark)
fig.subplots_adjust(left=0.025, right=0.975, top=0.85, bottom=0.055, wspace=0.16, hspace=0.37)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()