papers: rename folders and retitle

- Main paper: dual_decomposition_minimal_counterexamples/ ->
  face_monochromatic_pairs/. Title is now
  "Face-Monochromatic Pairs and the Four Colour Theorem".
- Companion paper: dual_decomposition_iterated_reduction/ ->
  iterated_reduction_in_reduced_dual/. Title is now
  "An Iterated Reduction in the Reduced Dual". Its prose and bibliography
  cite the parent under the new title.
- Update one absolute sys.path reference inside
  check_conj_face_kempe_n15.py that pointed at the old folder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:04:15 -04:00
parent 3d1b1eb4a3
commit 41227c6a0f
49 changed files with 40 additions and 35 deletions
@@ -0,0 +1,219 @@
"""Draw the iterated reduction algorithm's trace on the dodecahedron.
Produces three figures:
fig_alg_step0.png -- G' (dodecahedron) with F_v (inner pentagon) shaded.
fig_alg_step1.png -- H_1 (post step 1), 3-edge-coloured; 4 protected edges.
fig_alg_step2.png -- H_2 (post step 2), 3-edge-coloured; 8 protected edges;
algorithm terminates.
Run with: sage experiments/draw_iterated_reduction.py
"""
from sage.all import Graph
from sage.graphs.graph_coloring import edge_coloring
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import math
import os
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
C = ['#dc2626', '#16a34a', '#2563eb'] # proper-edge-colour palette
GRAY = '#9ca3af'
DARK = '#374151'
HIGHLIGHT = '#fef3c7'
def dodecahedron_positions():
pos = {}
R = {'a': 1.0, 'b': 2.2, 'c': 3.6, 'd': 4.8}
for i in range(5):
for fam in ('a', 'b'):
th = math.radians(90 - 72 * i)
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
for fam in ('c', 'd'):
th = math.radians(90 - 72 * i - 36)
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
return pos
def build_dodecahedron():
edges = []
for i in range(5):
edges.append((('a', i), ('a', (i + 1) % 5)))
edges.append((('a', i), ('b', i)))
edges.append((('b', i), ('c', i)))
edges.append((('b', i), ('c', (i - 1) % 5)))
edges.append((('c', i), ('d', i)))
edges.append((('d', i), ('d', (i + 1) % 5)))
G = Graph(edges, multiedges=False, loops=False)
G.is_planar(set_embedding=True)
return G
def find_safe_pentagonal_face(G, protected):
for face in G.faces():
if len(face) != 5:
continue
boundary = [u for (u, v) in face]
boundary_edges = [frozenset([u, v]) for (u, v) in face]
externals = []
A = []
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
else:
if not any(e in protected for e in boundary_edges + externals):
return boundary, externals, A
return None
def valid_indices(f_vec):
out = []
for i in range(5):
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
continue
if len({f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}) == 3:
out.append(i)
return out
def draw(ax, G, pos, *, coloring=None, protected=None,
shade_face=None):
if shade_face:
poly = [pos[v] for v in shade_face]
ax.add_patch(Polygon(poly, closed=True, facecolor=HIGHLIGHT,
edgecolor='none', zorder=0))
protected = protected or set()
for u, v in G.edges(labels=False):
e = frozenset([u, v])
c = C[coloring[e]] if coloring is not None else GRAY
lw = 3.8 if e in protected else 1.4
(x0, y0), (x1, y1) = pos[u], pos[v]
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
for v in G.vertices(sort=False):
x, y = pos[v]
if isinstance(v, tuple) and v[0] == 'v_n':
t = v[1]
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
edgecolors='black', linewidths=1.2, zorder=4)
ax.annotate(f'$v_n^{{({t})}}$', (x, y),
textcoords='offset points', xytext=(16, 16),
ha='left', fontsize=14, fontweight='bold',
color=DARK, zorder=6,
bbox=dict(boxstyle='round,pad=0.2', fc='white',
ec=DARK, lw=0.6))
else:
ax.scatter(x, y, s=70, color=DARK, zorder=3)
ax.set_aspect('equal')
ax.axis('off')
def main():
G = build_dodecahedron()
pos = dodecahedron_positions()
F_v = [('a', i) for i in range(5)]
# ----- Step 0: G' with F_v shaded -----
fig, ax = plt.subplots(figsize=(8, 8))
draw(ax, G, pos, shade_face=F_v)
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step0.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
# ----- Step 1: Definition 2.1 at F_v with i_1 = 0 -----
safe = find_safe_pentagonal_face(G, set())
boundary_1, externals_1, A_1 = safe
G1 = G.copy()
for v in boundary_1:
G1.delete_vertex(v)
v_n_1 = ('v_n', 1)
G1.add_vertex(v_n_1)
G1.add_edge(v_n_1, A_1[0])
G1.add_edge(v_n_1, A_1[1])
G1.add_edge(v_n_1, A_1[2])
G1.add_edge(A_1[3], A_1[4])
G1.is_planar(set_embedding=True)
pos1 = {v: p for v, p in pos.items() if v not in boundary_1}
cx = (pos[A_1[0]][0] + pos[A_1[1]][0] + pos[A_1[2]][0]) / 3
cy = (pos[A_1[0]][1] + pos[A_1[1]][1] + pos[A_1[2]][1]) / 3
pos1[v_n_1] = (cx * 0.55, cy * 0.55)
cols = edge_coloring(G1, value_only=False)
coloring = {}
for k, edge_list in enumerate(cols):
for u, v in edge_list:
coloring[frozenset([u, v])] = k
E = {
frozenset([v_n_1, A_1[1]]), # spike
frozenset([v_n_1, A_1[0]]), # side-0
frozenset([v_n_1, A_1[2]]), # side-1
frozenset([A_1[3], A_1[4]]), # merged
}
fig, ax = plt.subplots(figsize=(8, 8))
draw(ax, G1, pos1, coloring=coloring, protected=E)
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step1.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
# ----- Step 2: reduce at the only remaining safe face (outer pentagon) -----
safe = find_safe_pentagonal_face(G1, E)
if safe is None:
print("ERROR: expected an outer pentagonal face but none found.")
return
boundary_2, externals_2, A_2 = safe
f_vec = [coloring[e] for e in externals_2]
choices = valid_indices(f_vec)
if not choices:
print(f"ERROR: f-vector {f_vec} has no valid index.")
return
i_t = choices[0]
G2 = G1.copy()
for v in boundary_2:
G2.delete_vertex(v)
v_n_2 = ('v_n', 2)
G2.add_vertex(v_n_2)
G2.add_edge(v_n_2, A_2[i_t])
G2.add_edge(v_n_2, A_2[(i_t + 1) % 5])
G2.add_edge(v_n_2, A_2[(i_t + 2) % 5])
G2.add_edge(A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5])
G2.is_planar(set_embedding=True)
coloring2 = {e: c for e, c in coloring.items()
if not any(u in boundary_2 for u in e)}
side_0_2 = frozenset([v_n_2, A_2[i_t]])
spike_2 = frozenset([v_n_2, A_2[(i_t + 1) % 5]])
side_1_2 = frozenset([v_n_2, A_2[(i_t + 2) % 5]])
merged_2 = frozenset([A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5]])
coloring2[side_0_2] = coloring[externals_2[i_t]]
coloring2[spike_2] = coloring[externals_2[(i_t + 1) % 5]]
coloring2[side_1_2] = coloring[externals_2[(i_t + 2) % 5]]
coloring2[merged_2] = coloring[externals_2[(i_t + 3) % 5]]
pos2 = {v: p for v, p in pos1.items() if v not in boundary_2}
nbrs = [A_2[i_t], A_2[(i_t + 1) % 5], A_2[(i_t + 2) % 5]]
cx = sum(pos2[a][0] for a in nbrs) / 3
cy = sum(pos2[a][1] for a in nbrs) / 3
r = math.hypot(cx, cy)
# v_n^{(2)} lies outside the surviving graph (the deleted d's were outermost)
target_r = 5.0
pos2[v_n_2] = (cx * target_r / r, cy * target_r / r)
E |= {side_0_2, spike_2, side_1_2, merged_2}
fig, ax = plt.subplots(figsize=(8, 8))
draw(ax, G2, pos2, coloring=coloring2, protected=E)
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step2.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
print(f"Wrote fig_alg_step{{0,1,2}}.png to {OUT_DIR}")
if __name__ == '__main__':
main()
@@ -0,0 +1,520 @@
"""Draw the iterated reduction trace on the smallest triangulation where
the chord-apex + Kempe-cycle property is satisfied: the first min-degree-5
plantri triangulation on n = 14 vertices, found by search_kempe_property.py.
Overwrites fig_alg_step{0,1,2}.png in the paper directory with this
triangulation's trace (replacing the dodecahedron version).
Run with: sage experiments/draw_iterated_reduction_n14.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import math
import os
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
"""Tutte's barycentric embedding: pick the largest face whose vertex set
avoids `avoid_verts` as the outer face, place its vertices on a regular
polygon, then iterate each interior vertex to the barycenter of its
neighbors. For 3-connected planar graphs this converges to the unique
straight-line planar embedding with the chosen outer face --- balanced
by construction and free of edge crossings."""
avoid = set(avoid_verts or ())
candidates = []
for face in G_sage.faces():
verts = [u for (u, v) in face]
if not (set(verts) & avoid):
candidates.append(verts)
if not candidates:
outer = [u for (u, v) in max(G_sage.faces(), key=len)]
else:
outer = max(candidates, key=len)
n_outer = len(outer)
pos = {}
for k, v in enumerate(outer):
ang = 2 * math.pi * k / n_outer + math.pi / 2
pos[v] = (math.cos(ang), math.sin(ang))
interior = [v for v in G_sage.vertex_iterator() if v not in pos]
for v in interior:
pos[v] = (0.0, 0.0)
for _ in range(iterations):
new_pos = dict(pos)
for v in interior:
nbrs = list(G_sage.neighbor_iterator(v))
sx = sum(pos[w][0] for w in nbrs) / len(nbrs)
sy = sum(pos[w][1] for w in nbrs) / len(nbrs)
new_pos[v] = (sx, sy)
pos = new_pos
return pos
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
C = ['#dc2626', '#16a34a', '#2563eb']
GRAY = '#9ca3af'
DARK = '#374151'
HIGHLIGHT = '#fef3c7'
SHADE = '#fef3c7'
def dual_of(G):
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
e = frozenset((u, v))
edge_to_faces.setdefault(e, []).append(fi)
dual_edges = []
for e, fs in edge_to_faces.items():
if len(fs) == 2:
dual_edges.append((fs[0], fs[1]))
return Graph(dual_edges, multiedges=False, loops=False)
def apply_reduction(G, face, i, v_n_label):
boundary = [u for (u, v) in face]
if len(set(boundary)) != 5:
return None
A = []
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
return None
A.append(outer[0])
if len(set(A)) != 5:
return None
if A[(i + 3) % 5] == A[(i + 4) % 5]:
return None
H = G.copy()
for v in boundary:
H.delete_vertex(v)
H.add_vertex(v_n_label)
side_0 = (v_n_label, A[i % 5])
spike = (v_n_label, A[(i + 1) % 5])
side_1 = (v_n_label, A[(i + 2) % 5])
merged = (A[(i + 3) % 5], A[(i + 4) % 5])
H.add_edges([side_0, spike, side_1, merged])
if H.has_multiple_edges() or H.has_loops():
return None
if not H.is_planar(set_embedding=True):
return None
if not all(H.degree(v) == 3 for v in H.vertex_iterator()):
return None
named = {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
}
return H, named, boundary, A
def proper_3_edge_colorings(G):
edges = list(G.edges(labels=False))
n_edges = len(edges)
adj = [[] for _ in range(n_edges)]
for i in range(n_edges):
u, v = edges[i][0], edges[i][1]
for j in range(i):
x, y = edges[j][0], edges[j][1]
if u in (x, y) or v in (x, y):
adj[i].append(j)
adj[j].append(i)
coloring = [-1] * n_edges
def back(k):
if k == n_edges:
yield tuple(coloring)
return
for c in range(3):
if all(coloring[j] != c for j in adj[k]):
coloring[k] = c
yield from back(k + 1)
coloring[k] = -1
return edges, back(0)
def kempe_cycle(edges, coloring, start_idx, color_pair):
a, b = color_pair
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
if start_idx not in in_sub:
return None
visited = {start_idx}
stack = [start_idx]
while stack:
cur = stack.pop()
u, v = edges[cur][0], edges[cur][1]
for j in in_sub:
if j in visited:
continue
x, y = edges[j][0], edges[j][1]
if u in (x, y) or v in (x, y):
visited.add(j)
stack.append(j)
return visited
def matches_property(edges, col, named):
idx = {}
for ii, e in enumerate(edges):
es = frozenset((e[0], e[1]))
for role, ns in named.items():
if es == ns:
idx[role] = ii
if len(idx) != 4:
return False
c_spike = col[idx['spike']]
c_merged = col[idx['merged']]
if c_spike != c_merged:
return False
c_s0 = col[idx['side_0']]
c_s1 = col[idx['side_1']]
kc0 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s0))
if idx['side_0'] not in kc0 or idx['merged'] not in kc0:
return False
kc1 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s1))
if idx['side_1'] not in kc1 or idx['merged'] not in kc1:
return False
return True
def find_first_match():
"""Iterate over (G, face, i_red, coloring) and return the first hit."""
for G in graphs.triangulations(14, minimum_degree=5):
if not G.is_planar(set_embedding=True):
continue
D = dual_of(G)
D.is_planar(set_embedding=True)
for face in D.faces():
if len(face) != 5:
continue
for i_red in range(5):
res = apply_reduction(D, face, i_red, '__v_n_1__')
if res is None:
continue
H, named, boundary, A = res
edges, gen = proper_3_edge_colorings(H)
for col in gen:
if matches_property(edges, col, named):
coloring_dict = {frozenset((e[0], e[1])): c
for e, c in zip(edges, col)}
return G, D, face, i_red, H, named, boundary, A, coloring_dict
return None
def draw_graph(ax, G, pos, *, coloring=None, protected=None,
shade_vertices=None, vn_labels=None):
if shade_vertices:
poly = [pos[v] for v in shade_vertices]
ax.add_patch(Polygon(poly, closed=True, facecolor=SHADE,
edgecolor='none', zorder=0))
protected = protected or set()
vn_labels = vn_labels or {}
for u, v, _ in G.edges():
e = frozenset([u, v])
c = C[coloring[e]] if (coloring is not None and e in coloring) else GRAY
lw = 3.8 if e in protected else 1.4
(x0, y0), (x1, y1) = pos[u], pos[v]
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
for v in G.vertices(sort=False):
x, y = pos[v]
if v in vn_labels:
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
edgecolors='black', linewidths=1.2, zorder=4)
ax.annotate(vn_labels[v], (x, y),
textcoords='offset points', xytext=(16, 16),
ha='left', fontsize=14, fontweight='bold',
color=DARK, zorder=6,
bbox=dict(boxstyle='round,pad=0.2', fc='white',
ec=DARK, lw=0.6))
else:
ax.scatter(x, y, s=60, color=DARK, zorder=3)
ax.set_aspect('equal')
ax.axis('off')
def main():
print("Searching for the first match at n = 14 ...")
result = find_first_match()
if result is None:
print("No match found at n = 14.")
return
G14, D, face, i_red, H1, named1, boundary1, A1, coloring1 = result
print(f"Found at i_red = {i_red}")
print(f" G (n=14): |V|={G14.order()}, |E|={G14.size()}, "
f"min_deg={min(G14.degree())}")
print(f" D = G': |V|={D.order()}, |E|={D.size()}")
print(f" H_1: |V|={H1.order()}, |E|={H1.size()}")
# Relabel H_1 in place so all vertex labels are comparable integers
# (Sage's planar layout and face enumeration need comparable labels).
# Translate coloring1 and named1 accordingly.
H1_relabel_map = {v: i for i, v in enumerate(H1.vertex_iterator())}
H1.relabel(perm=H1_relabel_map, inplace=True)
vn1_int = H1_relabel_map['__v_n_1__']
coloring1 = {frozenset(H1_relabel_map[u] for u in e): c
for e, c in coloring1.items()}
named1 = {role: frozenset(H1_relabel_map[u] for u in e)
for role, e in named1.items()}
D.is_planar(set_embedding=True)
D_layout = tutte_layout(D, avoid_verts=set(u for (u, v) in face))
H1.is_planar(set_embedding=True)
H1_layout = tutte_layout(H1, avoid_verts={vn1_int})
boundary_face_verts = [u for (u, v) in face]
fig, ax = plt.subplots(figsize=(8, 8))
draw_graph(ax, D, D_layout, shade_vertices=boundary_face_verts)
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step0.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
print("Wrote fig_alg_step0.png")
E = set(named1.values())
fig, ax = plt.subplots(figsize=(8, 8))
draw_graph(ax, H1, H1_layout, coloring=coloring1, protected=E,
vn_labels={vn1_int: '$v_n^{(1)}$'})
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step1.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
print("Wrote fig_alg_step1.png")
# ----- Step 2: try to continue -----
H1.is_planar(set_embedding=True)
chosen2 = None
for face2 in H1.faces():
if len(face2) != 5:
continue
boundary2 = [u for (u, v) in face2]
boundary2_edges = [frozenset([u, v]) for (u, v) in face2]
externals2 = []
A2 = []
valid_face = True
for B_k in boundary2:
outer = [w for w in H1.neighbor_iterator(B_k) if w not in boundary2]
if len(outer) != 1:
valid_face = False
break
externals2.append(frozenset([B_k, outer[0]]))
A2.append(outer[0])
if not valid_face:
continue
if any(e in E for e in boundary2_edges + externals2):
continue
# find valid i_t
f_vec = [coloring1[e] for e in externals2]
for i_t in range(5):
if f_vec[(i_t + 3) % 5] != f_vec[(i_t + 4) % 5]:
continue
if len({f_vec[i_t], f_vec[(i_t + 1) % 5], f_vec[(i_t + 2) % 5]}) != 3:
continue
if A2[(i_t + 3) % 5] == A2[(i_t + 4) % 5]:
continue
chosen2 = (face2, i_t, boundary2, externals2, A2)
break
if chosen2 is not None:
break
if chosen2 is None:
# algorithm terminates at H_1
fig, ax = plt.subplots(figsize=(8, 8))
ax.text(0.5, 0.5,
"Algorithm terminates at $H_1$:\n"
"no pentagonal face of $H_1$ has all\n"
"ten incident edges outside $E$.",
ha='center', va='center', fontsize=18, color=DARK,
transform=ax.transAxes,
bbox=dict(boxstyle='round,pad=0.6', fc=HIGHLIGHT,
ec=DARK, lw=1.0))
ax.set_aspect('equal')
ax.axis('off')
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step2.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
print("Wrote fig_alg_step2.png (termination card)")
print(" Algorithm terminates at H_1: no safe pentagonal face.")
return
face2, i_t, boundary2, externals2, A2 = chosen2
print(f"Step 2: safe face found, i_t = {i_t}")
H2 = H1.copy()
for v in boundary2:
H2.delete_vertex(v)
# use a fresh int label for v_n^(2)
v_n_2 = max(H1.vertices(sort=False)) + 1
H2.add_vertex(v_n_2)
side_0_2 = (v_n_2, A2[i_t])
spike_2 = (v_n_2, A2[(i_t + 1) % 5])
side_1_2 = (v_n_2, A2[(i_t + 2) % 5])
merged_2 = (A2[(i_t + 3) % 5], A2[(i_t + 4) % 5])
H2.add_edges([side_0_2, spike_2, side_1_2, merged_2])
H2.is_planar(set_embedding=True)
coloring2 = {e: c for e, c in coloring1.items()
if not any(u in boundary2 for u in e)}
coloring2[frozenset(side_0_2)] = coloring1[externals2[i_t]]
coloring2[frozenset(spike_2)] = coloring1[externals2[(i_t + 1) % 5]]
coloring2[frozenset(side_1_2)] = coloring1[externals2[(i_t + 2) % 5]]
coloring2[frozenset(merged_2)] = coloring1[externals2[(i_t + 3) % 5]]
E |= {frozenset(side_0_2), frozenset(spike_2),
frozenset(side_1_2), frozenset(merged_2)}
H2_layout = tutte_layout(H2, avoid_verts={vn1_int, v_n_2})
fig, ax = plt.subplots(figsize=(8, 8))
draw_graph(ax, H2, H2_layout, coloring=coloring2, protected=E,
vn_labels={vn1_int: '$v_n^{(1)}$',
v_n_2: '$v_n^{(2)}$'})
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step2.png'),
dpi=170, bbox_inches='tight')
plt.close(fig)
print(f"Wrote fig_alg_step2.png: H_2 with |V|={H2.order()}, "
f"|E|={H2.size()}, |protected|={len(E)}")
# --- continue running to completion, checking Kempe condition each step --
print()
print("=" * 72)
print("Running algorithm to completion, checking chord-apex + Kempe at "
"each step.")
print("=" * 72)
# Step 1 status (by construction this is the matching coloring)
cond1 = check_step_conditions(H1, coloring1, named1)
print(f" step t = 1: |V|={H1.order():>3}, |E_graph|={H1.size():>3}, "
f"|E_prot|= 4 (initial)"
f" | chord-apex: {cond1['chord_apex']}, "
f"side_0-Kempe: {cond1['kc_side_0']}, "
f"side_1-Kempe: {cond1['kc_side_1']}")
run_to_completion_from(H2, coloring2, E,
{'spike': frozenset(spike_2),
'side_0': frozenset(side_0_2),
'side_1': frozenset(side_1_2),
'merged': frozenset(merged_2)},
start_t=2)
def check_step_conditions(H, coloring, named):
"""Given an H_t and the *just-added* spike/side_0/side_1/merged, check
whether chord-apex and the two Kempe-cycle conditions hold."""
edges = list(H.edges(labels=False))
edges_fs = [frozenset((u, v)) for (u, v) in edges]
col = [coloring[e] for e in edges_fs]
idx = {role: edges_fs.index(e) for role, e in named.items()}
c_spike = col[idx['spike']]
c_merged = col[idx['merged']]
chord_apex = (c_spike == c_merged)
if not chord_apex:
return {'chord_apex': False, 'kc_side_0': False, 'kc_side_1': False}
c_s0 = col[idx['side_0']]
c_s1 = col[idx['side_1']]
kc0 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s0))
kc1 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s1))
kc_side_0 = (idx['side_0'] in kc0 and idx['merged'] in kc0)
kc_side_1 = (idx['side_1'] in kc1 and idx['merged'] in kc1)
return {'chord_apex': True, 'kc_side_0': kc_side_0, 'kc_side_1': kc_side_1}
def find_safe_face(H, protected):
"""Return (face, externals, A) for some safe pentagonal face avoiding
`protected`, or None."""
for face in H.faces():
if len(face) != 5:
continue
boundary = [u for (u, v) in face]
boundary_edges = [frozenset([u, v]) for (u, v) in face]
externals = []
A = []
valid = True
for B_k in boundary:
outer = [w for w in H.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
valid = False
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
if not valid:
continue
if any(e in protected for e in boundary_edges + externals):
continue
return face, boundary, externals, A
return None
def run_to_completion_from(H, coloring, E, last_named, start_t):
"""Continue iterating from H_{start_t}. The 'last_named' dict carries
the spike/side/merged of step `start_t` so we can report its Kempe
status. Print a row per step."""
t = start_t
print(f" step t = {t}: |V|={H.order():>3}, |E_graph|={H.size():>3}, "
f"|E_prot|={len(E):>3}", end='')
cond = check_step_conditions(H, coloring, last_named)
print(f" | chord-apex: {cond['chord_apex']}, "
f"side_0-Kempe: {cond['kc_side_0']}, "
f"side_1-Kempe: {cond['kc_side_1']}")
while True:
H.is_planar(set_embedding=True)
res = find_safe_face(H, E)
if res is None:
print(f" step t = {t + 1}: no safe pentagonal face --> "
f"algorithm terminates at H_{t}.")
return
face, boundary, externals, A = res
f_vec = [coloring[e] for e in externals]
i_t = None
for i in range(5):
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
continue
if len({f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}) != 3:
continue
if A[(i + 3) % 5] == A[(i + 4) % 5]:
continue
i_t = i
break
if i_t is None:
print(f" step t = {t + 1}: f = {f_vec}, no valid index --> "
f"terminate (Lemma 2.4 violation? Probably a parallel-edge "
f"or other degenerate case).")
return
t += 1
v_n_new = max(H.vertices(sort=False)) + 1 if all(
isinstance(v, int) for v in H.vertex_iterator()) else f'vn{t}'
H_new = H.copy()
for v in boundary:
H_new.delete_vertex(v)
H_new.add_vertex(v_n_new)
side_0 = (v_n_new, A[i_t])
spike = (v_n_new, A[(i_t + 1) % 5])
side_1 = (v_n_new, A[(i_t + 2) % 5])
merged = (A[(i_t + 3) % 5], A[(i_t + 4) % 5])
H_new.add_edges([side_0, spike, side_1, merged])
H = H_new
coloring = {e: c for e, c in coloring.items()
if not any(u in boundary for u in e)}
coloring[frozenset(side_0)] = coloring[externals[i_t]] \
if frozenset(externals[i_t]) in coloring else f_vec[i_t]
# safer: directly use f_vec
coloring[frozenset(side_0)] = f_vec[i_t]
coloring[frozenset(spike)] = f_vec[(i_t + 1) % 5]
coloring[frozenset(side_1)] = f_vec[(i_t + 2) % 5]
coloring[frozenset(merged)] = f_vec[(i_t + 3) % 5]
named = {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
}
E |= set(named.values())
cond = check_step_conditions(H, coloring, named)
print(f" step t = {t}: |V|={H.order():>3}, |E_graph|={H.size():>3}, "
f"|E_prot|={len(E):>3}, i_t = {i_t}", end='')
print(f" | chord-apex: {cond['chord_apex']}, "
f"side_0-Kempe: {cond['kc_side_0']}, "
f"side_1-Kempe: {cond['kc_side_1']}")
if __name__ == '__main__':
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

@@ -0,0 +1,34 @@
\relax
\citation{parent}
\citation{parent}
\citation{parent}
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Setup and background}}{1}{}\protected@file@percent }
\newlabel{sec:background}{{1}{1}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{The iterated reduction}}{1}{}\protected@file@percent }
\newlabel{sec:iterated-reduction}{{2}{1}}
\newlabel{alg:iterated-reduction}{{2.1}{1}}
\citation{parent}
\citation{parent}
\citation{parent}
\citation{parent}
\citation{parent}
\newlabel{rem:alg-invariants}{{2.2}{2}}
\newlabel{rem:alg-chord-apex}{{2.3}{2}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Structural lemmas on the algorithm's output}}{2}{}\protected@file@percent }
\newlabel{sec:structural-lemmas}{{3}{2}}
\newlabel{lem:exactly-one-match}{{3.1}{2}}
\citation{parent}
\citation{parent}
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Algorithm\nonbreakingspace 2.1\hbox {} on $G'=\mathrm {dual}(G)$, where $G$ is the first min-degree-$5$ plantri triangulation on $14$ vertices and $\varphi _1$ is a specific proper $3$-edge-colouring of $H_1$ that satisfies both the chord-apex and Kempe-cycle conditions of\nonbreakingspace \cite {parent}, found by \texttt {experiments/search\_kempe\_property.py}. \emph {Left:} $G'$ ($24$ vertices, $36$ edges) with the chosen pentagonal face shaded. \emph {Centre:} $H_1$ ($20$ vertices, $30$ edges) after step\nonbreakingspace (1) with $i_1 = 1$, $3$-edge-coloured by $\varphi _1$; the four edges around $v_n^{(1)}$ in $E$ are drawn thicker, and the spike and merged edges share the colour green. \emph {Right:} $H_2$ ($16$ vertices, $24$ edges) after step\nonbreakingspace (3) with $i_t = 3$; eight edges are protected, and the algorithm terminates one step later (no remaining safe pentagonal face in $H_2$). The generating script is \texttt {experiments/draw\_iterated\_reduction\_n14.py}; layouts are Tutte barycentric embeddings with the outer face picked to keep $v_n^{(1)}, v_n^{(2)}$ in the interior.}}{3}{}\protected@file@percent }
\newlabel{fig:iterated-reduction-trace}{{1}{3}}
\newlabel{lem:all-distinct-exists}{{3.2}{3}}
\citation{parent}
\citation{parent}
\bibcite{parent}{1}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{4}{}\protected@file@percent }
\gdef \@abspage@last{4}
@@ -0,0 +1,247 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 24 MAY 2026 15:04
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**paper.tex
(./paper.tex
LaTeX2e <2021-11-15> patch level 1
L3 programming layer <2022-02-24>
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
Document Class: amsart 2020/05/29 v2.20.6
\linespacing=\dimen138
\normalparindent=\dimen139
\normaltopskip=\skip47
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
Package: amsmath 2021/10/15 v2.17l AMS math features
\@mathmargin=\skip48
For additional information on amsmath, use the `?' option.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks16
\ex@=\dimen140
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen141
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names
)
\inf@bad=\count185
LaTeX Info: Redefining \frac on input line 234.
\uproot@=\count186
\leftroot@=\count187
LaTeX Info: Redefining \overline on input line 399.
\classnum@=\count188
\DOTSCASE@=\count189
LaTeX Info: Redefining \ldots on input line 496.
LaTeX Info: Redefining \dots on input line 499.
LaTeX Info: Redefining \cdots on input line 620.
\Mathstrutbox@=\box50
\strutbox@=\box51
\big@size=\dimen142
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
\macc@depth=\count190
\c@MaxMatrixCols=\count191
\dotsspace@=\muskip16
\c@parentequation=\count192
\dspbrk@lvl=\count193
\tag@help=\toks17
\row@=\count194
\column@=\count195
\maxfields@=\count196
\andhelp@=\toks18
\eqnshift@=\dimen143
\alignsep@=\dimen144
\tagshift@=\dimen145
\tagwidth@=\dimen146
\totwidth@=\dimen147
\lineht@=\dimen148
\@envbody=\toks19
\multlinegap=\skip49
\multlinetaggap=\skip50
\mathdisplay@stack=\toks20
LaTeX Info: Redefining \[ on input line 2938.
LaTeX Info: Redefining \] on input line 2939.
)
LaTeX Font Info: Trying to load font information for U+msa on input line 397
.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4
\symAMSb=\mathgroup5
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
)
\copyins=\insert199
\abstractbox=\box52
\listisep=\skip51
\c@part=\count197
\c@section=\count198
\c@subsection=\count266
\c@subsubsection=\count267
\c@paragraph=\count268
\c@subparagraph=\count269
\c@figure=\count270
\c@table=\count271
\abovecaptionskip=\skip52
\belowcaptionskip=\skip53
\captionindent=\dimen149
\thm@style=\toks21
\thm@bodyfont=\toks22
\thm@headfont=\toks23
\thm@notefont=\toks24
\thm@headpunct=\toks25
\thm@preskip=\skip54
\thm@postskip=\skip55
\thm@headsep=\skip56
\dth@everypar=\toks26
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
\KV@toks@=\toks27
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 107.
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
))
\Gin@req@height=\dimen150
\Gin@req@width=\dimen151
)
\c@theorem=\count272
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count273
\l__pdf_internal_box=\box53
)
(./paper.aux)
\openout1 = `paper.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Trying to load font information for U+msa on input line 27.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
LaTeX Font Info: Trying to load font information for U+msb on input line 27.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
)
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count274
\scratchdimen=\dimen152
\scratchbox=\box54
\nofMPsegments=\count275
\nofMParguments=\count276
\everyMPshowfont=\toks28
\MPscratchCnt=\count277
\MPscratchDim=\dimen153
\MPnumerator=\count278
\makeMPintoPDFobject=\count279
\everyMPtoPDFconversion=\toks29
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
85.
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
<fig_alg_step0.png, id=18, 399.6106pt x 459.55217pt>
File: fig_alg_step0.png Graphic file (type png)
<use fig_alg_step0.png>
Package pdftex.def Info: fig_alg_step0.png used on input line 170.
(pdftex.def) Requested size: 115.20264pt x 132.48134pt.
<fig_alg_step1.png, id=20, 399.6106pt x 459.55217pt>
File: fig_alg_step1.png Graphic file (type png)
<use fig_alg_step1.png>
Package pdftex.def Info: fig_alg_step1.png used on input line 171.
(pdftex.def) Requested size: 115.20264pt x 132.48134pt.
<fig_alg_step2.png, id=21, 399.6106pt x 459.55217pt>
File: fig_alg_step2.png Graphic file (type png)
<use fig_alg_step2.png>
Package pdftex.def Info: fig_alg_step2.png used on input line 172.
(pdftex.def) Requested size: 115.20264pt x 132.48134pt.
LaTeX Warning: `h' float specifier changed to `ht'.
[2] [3 <./fig_alg_step0.png> <./fig_alg_step1.png> <./fig_alg_step2.png>]
[4] (./paper.aux) )
Here is how much of TeX's memory you used:
3022 strings out of 478268
42273 string characters out of 5846347
343206 words of memory out of 5000000
21067 multiletter control sequences out of 15000+600000
475834 words of font info for 54 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
69i,7n,76p,1255b,294s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/
cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cm
r5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.
pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb>
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb></
usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></us
r/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr
/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/l
ocal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/lo
cal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (4 pages, 385985 bytes).
PDF statistics:
109 PDF objects out of 1000 (max. 8388607)
62 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
16 words of extra memory for PDF output out of 10000 (max. 10000000)
Binary file not shown.
@@ -0,0 +1,308 @@
%% filename: amsart-template.tex
%% American Mathematical Society
%% AMS-LaTeX v.2 template for use with amsart
%% ====================================================================
\documentclass{amsart}
\usepackage{amssymb}
\usepackage{graphicx}
\newtheorem{theorem}{Theorem}[section]
\newtheorem{lemma}[theorem]{Lemma}
\newtheorem{corollary}[theorem]{Corollary}
\newtheorem{proposition}[theorem]{Proposition}
\newtheorem{conjecture}[theorem]{Conjecture}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\newtheorem{algorithm}[theorem]{Algorithm}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
\numberwithin{equation}{section}
\begin{document}
\title{An Iterated Reduction in the Reduced Dual}
\author{Eric Bauerfeld}
\address{}
\curraddr{}
\email{}
\thanks{}
\subjclass[2010]{Primary }
\keywords{four colour theorem, plane triangulation, dual graph, cubic planar
graph, edge connectivity, cyclic edge cut, Tait colouring,
$3$-edge-colouring}
\date{}
\dedicatory{}
\begin{abstract}
% TODO: abstract.
\end{abstract}
\maketitle
\section{Setup and background}
\label{sec:background}
This paper is a follow-up to \emph{Face-Monochromatic Pairs and the Four
Colour Theorem}~\cite{parent}, which introduced the reduced-dual
construction: given a minimal counterexample $G$ to the Four Colour
Theorem, a degree-$5$ vertex $v$ of $G$ (equivalently a pentagonal face
$F_v$ of $G' = \mathrm{dual}(G)$), and an index $i \in \{0,1,2,3,4\}$, the
\emph{reduced dual} $\widehat{G}'_{v,i}$ is the cubic plane graph obtained
from $G'$ by deleting the five boundary vertices of $F_v$, listing the
resulting five degree-$2$ vertices clockwise as $A_0,\dots,A_4$ along the
new face $F$, attaching a new apex vertex $v_n$ to $A_i, A_{i+1}, A_{i+2}$
by three new edges, and adding the chord $A_{i+3} A_{i+4}$. The four edges
added by steps~(3) and~(4) are named the \emph{side-$0$ edge}
($v_n A_i$), the \emph{spike edge} ($v_n A_{i+1}$), the \emph{side-$1$
edge} ($v_n A_{i+2}$), and the \emph{merged edge} ($A_{i+3} A_{i+4}$). The
parent paper also proves two structural lemmas about every proper
$3$-edge-colouring $\varphi$ of $\widehat{G}'_{v,i}$:
\begin{itemize}
\item the \emph{chord-apex lemma}, asserting
$\varphi(\mathrm{spike}) = \varphi(\mathrm{merged})$;
\item the \emph{Kempe-cycle lemma}, asserting that
the spike and merged edge lie on a common
$\{\varphi(\mathrm{spike}), \varphi(\mathrm{side-}j)\}$-Kempe
cycle through the side-$j$ edge for both $j = 0, 1$.
\end{itemize}
We refer the reader to~\cite{parent} for the precise definitions, proofs,
and the pentagonal-externals lemma we will reuse below.
\section{The iterated reduction}
\label{sec:iterated-reduction}
The reduced-dual construction can be iterated: starting from a proper
$3$-edge-colouring $\varphi_1$ of a reduced dual $\widehat{G}'_{v,i}$, we
apply the construction again to that graph at a pentagonal face whose ten
incident edges avoid the four named edges from the first reduction,
extending $\varphi_1$ across the new reduction. The protected edges
accumulate into a set $E$ that grows by four per iteration, and the
process terminates when $E$ has blocked every pentagonal face.
\begin{algorithm}[Iterated reduction with protected edges]
\label{alg:iterated-reduction}
Let $G$ be a triangulation we assume to be a minimal counterexample to the
Four Colour Theorem. The algorithm produces a sequence $H_1, H_2, \dots$ of
cubic plane graphs, proper $3$-edge-colourings $\varphi_t$ of $H_t$, and a
growing set $E$ of protected edges.
\begin{enumerate}
\item[(0)] Form $G' := \mathrm{dual}(G)$, a cubic plane graph.
\item[(1)] Choose a degree-$5$ vertex $v$ of $G$ (equivalently a
pentagonal face $F_v$ of $G'$) and an index
$i_1 \in \{0, \dots, 4\}$. Apply the reduced-dual construction
of~\cite{parent} to form $H_1 := \widehat{G'}_{v, i_1}$, and fix
any proper $3$-edge-colouring $\varphi_1$ of $H_1$ (one exists
by the minimality of $G$).
\item[(2)] Initialise
$E := \{\text{spike}, \text{side-}0, \text{side-}1,
\text{merged}\}$, the four named edges of the reduction in~(1).
\item[(3)] (Iterate.) At step $t \geq 2$, given $H_{t-1}$,
$\varphi_{t-1}$, and $E \subseteq E(H_{t-1})$:
\begin{enumerate}
\item[(a)] Find a pentagonal face $F$ of $H_{t-1}$ whose ten
incident edges --- the five boundary edges of $\partial F$
and the five external edges at $\partial F$ --- are all
outside $E$. If no such $F$ exists, terminate.
\item[(b)] By the pentagonal-externals lemma of~\cite{parent}
applied to $H_{t-1}$ at $F$ under $\varphi_{t-1}$, the
external vector has shape $(a, b, c, c, c)$ up to cyclic
rotation. Choose an index $i_t$ for which
$\varphi_{t-1}(f_{i_t + 3}) = \varphi_{t-1}(f_{i_t + 4})$
and $\varphi_{t-1}(f_{i_t})$, $\varphi_{t-1}(f_{i_t + 1})$,
$\varphi_{t-1}(f_{i_t + 2})$ are three distinct colours.
\item[(c)] Apply the reduced-dual construction
of~\cite{parent} to $H_{t-1}$ at $(F, i_t)$ to form $H_t$.
\item[(d)] Extend $\varphi_{t-1}$ to a proper
$3$-edge-colouring $\varphi_t$ of $H_t$: every surviving
edge keeps its $\varphi_{t-1}$-colour, and each new edge
takes the unique colour completing the palette at its
endpoint (consistent across both endpoints of the chord
by the choice of $i_t$).
\item[(e)] Add the four named edges of the step-$t$ reduction
to $E$.
\end{enumerate}
\item[(4)] Repeat (3) until termination.
\end{enumerate}
\end{algorithm}
\begin{remark}
\label{rem:alg-invariants}
At each iteration, $|V(H_t)| = |V(H_{t-1})| - 4$ and
$|E(H_t)| = |E(H_{t-1})| - 6$, so $H_t$ shrinks at a fixed rate; the
protected set $|E|$ grows by exactly four; and every protected edge
survives all subsequent reductions. Since the graph is finite,
termination is guaranteed. By the pentagonal-externals lemma
of~\cite{parent}, step~(b) never fails: some valid $i_t$ always exists
for any pentagonal face under any proper colouring. Termination is
therefore combinatorial: it occurs precisely when $E$ touches every
pentagonal face of $H_{t-1}$.
\end{remark}
\begin{remark}
\label{rem:alg-chord-apex}
The chord-apex lemma of~\cite{parent} applies only at $t = 1$, when $H_1$
is a reduced dual of $G'$. For $t \geq 2$, $H_t$ is a reduced dual of
$H_{t-1}$ rather than of $G'$, and $H_{t-1}$ is itself
$3$-edge-colourable, so the non-$3$-edge-colourability argument that
drives the chord-apex lemma does not carry over. Whether the constraints
accumulated in $E$ propagate any further structure to $\varphi_t$ for
$t \geq 2$ is left open.
\end{remark}
\begin{figure}[h]
\centering
\includegraphics[width=0.32\textwidth]{fig_alg_step0.png}\hfill
\includegraphics[width=0.32\textwidth]{fig_alg_step1.png}\hfill
\includegraphics[width=0.32\textwidth]{fig_alg_step2.png}
\caption{Algorithm~\ref{alg:iterated-reduction} on
$G'=\mathrm{dual}(G)$, where $G$ is the first min-degree-$5$ plantri
triangulation on $14$ vertices and $\varphi_1$ is a specific proper
$3$-edge-colouring of $H_1$ that satisfies both the chord-apex and
Kempe-cycle conditions of~\cite{parent}, found by
\texttt{experiments/search\_kempe\_property.py}. \emph{Left:} $G'$
($24$ vertices, $36$ edges) with the chosen pentagonal face shaded.
\emph{Centre:} $H_1$ ($20$ vertices, $30$ edges) after step~(1) with
$i_1 = 1$, $3$-edge-coloured by $\varphi_1$; the four edges around
$v_n^{(1)}$ in $E$ are drawn thicker, and the spike and merged edges
share the colour green. \emph{Right:} $H_2$ ($16$ vertices, $24$ edges)
after step~(3) with $i_t = 3$; eight edges are protected, and the
algorithm terminates one step later (no remaining safe pentagonal face
in $H_2$). The generating script is
\texttt{experiments/draw\_iterated\_reduction\_n14.py}; layouts are
Tutte barycentric embeddings with the outer face picked to keep
$v_n^{(1)}, v_n^{(2)}$ in the interior.}
\label{fig:iterated-reduction-trace}
\end{figure}
\section{Structural lemmas on the algorithm's output}
\label{sec:structural-lemmas}
\begin{lemma}[Exactly one matching pair in the algorithm's output]
\label{lem:exactly-one-match}
Let $G$ be a minimal counterexample to the Four Colour Theorem, and let
$(H_{t^*}, \varphi_{t^*})$ be the final graph-and-colouring produced by
some terminating execution of
Algorithm~\ref{alg:iterated-reduction} on $G$, with named pairs
$(\mathrm{spike}_t, \mathrm{merged}_t)$ for $t = 1, \dots, t^*$. Then
there is exactly one $t$ with
$\varphi_{t^*}(\mathrm{spike}_t) = \varphi_{t^*}(\mathrm{merged}_t)$, and
it is $t = 1$.
\end{lemma}
\begin{proof}
The algorithm never re-colours an existing edge: at each iteration
step~(3d) every surviving edge keeps its $\varphi_{t-1}$-colour, and the
four new edges receive fresh colours forced by propriety. Hence for every
$1 \leq k \leq t \leq t^*$,
\[
\varphi_t(\mathrm{spike}_k) = \varphi_k(\mathrm{spike}_k),
\qquad
\varphi_t(\mathrm{merged}_k) = \varphi_k(\mathrm{merged}_k);
\]
the colours of the step-$k$ named edges, once written, are permanent. It
suffices to compare $\varphi_k(\mathrm{spike}_k)$ and
$\varphi_k(\mathrm{merged}_k)$ at the step where each pair is introduced.
\textbf{Case $k = 1$.} Since $G$ is a minimal counterexample, $H_1$ is a
reduced dual of $G'$. The chord-apex lemma of~\cite{parent} applied to
$\varphi_1$ gives
$\varphi_1(\mathrm{spike}_1) = \varphi_1(\mathrm{merged}_1)$.
\textbf{Case $k \geq 2$.} At step $k$ the algorithm picks an index $i_k$
for which $f_{i_k+3} = f_{i_k+4}$ (chord consistency) and
$f_{i_k}, f_{i_k+1}, f_{i_k+2}$ are pairwise distinct (propriety at the
new $v_n$), where $f$ is the external vector of the chosen pentagonal
face of $H_{k-1}$ under $\varphi_{k-1}$. Step~(3d) then assigns
\[
\varphi_k(\mathrm{spike}_k) = f_{i_k+1},
\qquad
\varphi_k(\mathrm{merged}_k) = f_{i_k+3}.
\]
By the pentagonal-externals lemma of~\cite{parent}, $f$ has the $(2,2,1)$
pattern: a block of three consecutive positions $\{p, p+1, p+2\}$
(mod $5$) on which it is constantly some colour $c$, while the remaining
two positions $\{p+3, p+4\}$ hold the two non-$c$ colours, one each. The
condition $f_{i_k+3} = f_{i_k+4}$ forces $(i_k+3, i_k+4)$ to be either
$(p, p+1)$ or $(p+1, p+2)$ --- the two consecutive pairs inside the
block --- and correspondingly $i_k + 1 \in \{p+3, p+4\}$,
\emph{outside} the block. So $f_{i_k+1}$ is not $c$, whereas
$f_{i_k+3} = c$, and hence
$\varphi_k(\mathrm{spike}_k) \neq \varphi_k(\mathrm{merged}_k)$.
Combining the two cases, exactly one $t \in \{1, \dots, t^*\}$ --- namely
$t = 1$ --- has $\varphi_{t^*}(\mathrm{spike}_t) =
\varphi_{t^*}(\mathrm{merged}_t)$.
\end{proof}
\begin{lemma}[All-distinct colouring exists on a 4-colourable graph]
\label{lem:all-distinct-exists}
Let $G$ be a $4$-colourable maximal planar graph of minimum degree
$\geq 5$ (equivalently, a maximal planar graph that is \emph{not} a
minimal counterexample to the Four Colour Theorem). Then there is an
execution of Algorithm~\ref{alg:iterated-reduction} on $G$ whose final
colouring $\varphi_{t^*}$ satisfies
$\varphi_{t^*}(\mathrm{spike}_t) \neq \varphi_{t^*}(\mathrm{merged}_t)$
for every $t \in \{1, \dots, t^*\}$. In particular, there exists a proper
$3$-edge-colouring of $H_{t^*}$ under which every spike-merged pair has
distinct colours.
\end{lemma}
\begin{proof}
The argument mirrors Lemma~\ref{lem:exactly-one-match}, but extends a
colouring \emph{downward} from $G'$ rather than carrying one forward from
$H_1$.
Since $G$ is $4$-colourable, by Tait's theorem
$G' = \mathrm{dual}(G)$ admits a proper $3$-edge-colouring $\xi$. Apply
the pentagonal-externals lemma of~\cite{parent} to $\xi$ at the
pentagonal face $F_v$ selected in step~(1): the external vector
$f = (f_0, \dots, f_4)$ at $F_v$ under $\xi$ has the $(3,1,1)$
cyclic-consecutive shape, with a block of three consecutive positions
$\{p, p+1, p+2\}$ (mod $5$) holding a common colour $c$, and the
remaining two positions $\{p+3, p+4\}$ holding the two non-$c$ colours,
one each. The algorithm's choice of $i_1$ forces $\{i_1+3, i_1+4\}$
inside the $c$-block (so the chord is consistently coloured) and the
three positions $\{i_1, i_1+1, i_1+2\}$ pairwise distinct; in particular
$i_1+1$ lies \emph{outside} the $c$-block.
Choose $\varphi_1$ to be the proper $3$-edge-colouring of $H_1$ that
agrees with $\xi$ on every surviving edge and assigns each new edge at
$A_j$ the unique third colour at $A_j$. Then
$\varphi_1(\mathrm{spike}_1) = f_{i_1+1}$, a value not equal to $c$,
while $\varphi_1(\mathrm{merged}_1) = f_{i_1+3} = c$, so
$\varphi_1(\mathrm{spike}_1) \neq \varphi_1(\mathrm{merged}_1)$.
The same argument repeats at every step $k \geq 2$: the external vector
at the chosen pentagonal face under $\varphi_{k-1}$ has the $(3,1,1)$
cyclic-consecutive shape (pentagonal-externals lemma of~\cite{parent}),
the algorithm's index choice $i_k$ puts $i_k+3, i_k+4$ inside the colour
block and $i_k+1$ outside, and step~(3d) thus assigns
$\varphi_k(\mathrm{spike}_k) \neq \varphi_k(\mathrm{merged}_k)$. The
algorithm preserves these colours through every later step, so
$\varphi_{t^*}(\mathrm{spike}_t) \neq \varphi_{t^*}(\mathrm{merged}_t)$
for every $t \in \{1, \dots, t^*\}$.
\end{proof}
\begin{thebibliography}{9}
\bibitem{parent}
E.~Bauerfeld, \emph{Face-Monochromatic Pairs and the Four Colour Theorem}.
Companion paper.
\end{thebibliography}
\end{document}