codexarticulation-without-bridges.py
articulation-without-bridges.py
"""Two triangles share a cut vertex, but have no bridges.
Tested with Python 3.14.4, numpy 2.3.5 and matplotlib 3.10.7.
Run from _media: MPLCONFIGDIR=/tmp/matplotlib python3 ../articulation-without-bridges.py
Writes articulation-without-bridges.png in the current working directory.
"""
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
VERTICES = "ABCDE"
EDGES = ("AB", "AC", "BC", "AD", "AE", "DE")
POSITIONS = dict(zip(VERTICES, np.array([
[0, 0], [-1.55, 0.95], [-1.55, -0.95], [1.55, 0.95], [1.55, -0.95]
])))
DARK, BLUE, GREEN, RED = "#263747", "#326f9c", "#25846a", "#bd3f43"
def adjacency(vertices, edges):
return {v: sorted(b if a == v else a for a, b in edges if v in (a, b))
for v in vertices}
def components(vertices, edges, removed_vertex=None, removed_edge=None):
remaining = set(vertices) - {removed_vertex}
kept = [e for e in edges if (removed_vertex is None or removed_vertex not in e)
and frozenset(e) != frozenset(removed_edge or ())]
neighbours = adjacency(sorted(remaining), kept)
groups = []
while remaining:
start = min(remaining)
remaining.remove(start)
pending, group = [start], []
while pending:
v = pending.pop()
group.append(v)
for w in neighbours[v]:
if w in remaining:
remaining.remove(w)
pending.append(w)
groups.append(sorted(group))
return groups
def compute():
original = components(VERTICES, EDGES)
deletions = {v: components(VERTICES, EDGES, removed_vertex=v) for v in VERTICES}
edge_counts = {e: len(components(VERTICES, EDGES, removed_edge=e)) for e in EDGES}
cut_vertices = [v for v in VERTICES if len(deletions[v]) > len(original)]
bridges = [e for e in EDGES if edge_counts[e] > len(original)]
assert original == [list(VERTICES)]
assert deletions["A"] == [list("BC"), list("DE")]
assert [len(deletions[v]) for v in VERTICES] == [2, 1, 1, 1, 1]
assert cut_vertices == ["A"] and bridges == []
# Isolated vertices count, including the empty-graph boundary case.
assert len(components(VERTICES + "Z", EDGES)) == 2
assert len(components(VERTICES + "Z", EDGES, removed_vertex="A")) == 3
assert len(components(VERTICES + "Z", EDGES, removed_vertex="Z")) == 1
assert components("Z", (), removed_vertex="Z") == []
return {"adjacency": adjacency(VERTICES, EDGES), "components": original,
"vertex_deletions": deletions, "edge_deletion_counts": edge_counts,
"cut_vertices": cut_vertices, "bridges": bridges}
def draw(ax, deleted, title):
groups = components(VERTICES, EDGES, removed_vertex=deleted)
colors = {v: (BLUE if i == 0 else GREEN) for i, group in enumerate(groups) for v in group}
for a, b in EDGES:
if deleted not in (a, b):
p, q = POSITIONS[a], POSITIONS[b]
ax.plot([p[0], q[0]], [p[1], q[1]], color=colors[a], lw=2.6, zorder=1)
for v in VERTICES:
if v == deleted:
continue
x, y = POSITIONS[v]
color = RED if v == "A" else colors[v]
ax.scatter(x, y, s=980, color="white", edgecolor=color, linewidth=2.8, zorder=2)
ax.text(x, y, v, ha="center", va="center", fontsize=15, weight="bold", color=color)
if deleted:
ax.text(0, 0, "A removed", ha="center", va="center", fontsize=11, color=RED)
ax.set_title(title, fontsize=13, color=DARK, pad=14)
ax.set_xlim(-2.05, 2.05)
ax.set_ylim(-1.5, 1.5)
ax.set_aspect("equal")
ax.axis("off")
def main():
result = compute()
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11})
fig, axes = plt.subplots(1, 2, figsize=(8.4, 3.65), facecolor="white")
fig.suptitle("A vulnerable vertex, despite an alternative route around every edge",
fontsize=14, color=DARK, y=0.97)
draw(axes[0], None, "Original: 1 connected component")
draw(axes[1], "A", "Delete vertex A: 2 connected components")
fig.text(0.25, 0.09, "Cut vertices: A Bridges: none", ha="center", color=DARK)
fig.text(0.75, 0.09, "Surviving components: {B, C} and {D, E}", ha="center", color=DARK)
fig.subplots_adjust(left=0.025, right=0.975, top=0.79, bottom=0.18, wspace=0.13)
fig.savefig(Path.cwd() / "articulation-without-bridges.png", dpi=140, facecolor="white",
transparent=False)
plt.close(fig)
print(json.dumps(result, sort_keys=True))
if __name__ == "__main__":
main()