dual_decomposition: iterated-reduction algorithm + Kempe/chord-apex search

- Add section 3 with Algorithm 3.1 (iterated reduction with protected edges)
  and remarks on invariants and chord-apex applicability.
- Add fig:iterated-reduction-trace illustrating the algorithm on G' =
  dodecahedron (G' -> H_1 -> H_2 -> terminate).
- experiments/iterated_reduction.py: Sage implementation of the algorithm.
- experiments/draw_iterated_reduction.py: produces the 3 trace figures.
- experiments/check_dodecahedron_kempe.py: enumerate proper 3-edge-colorings
  of the dodecahedron's reduced dual and check the chord-apex + Kempe-cycle
  conditions (0 of 36 colorings satisfy all three).
- experiments/search_kempe_property.py: search across min-deg-5
  triangulations; the n = 14 first plantri triangulation is the smallest hit
  (reduced dual has 20 v, 30 e).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 12:40:38 -04:00
parent 192ad33bd2
commit c987259c14
11 changed files with 1057 additions and 55 deletions
@@ -0,0 +1,186 @@
"""Check whether the dodecahedron's reduced dual admits a proper
3-edge-colouring with:
(i) colour(spike) == colour(merged), AND
(ii) the {c_spike, c_side0}-Kempe cycle through the spike contains both
side-0 and the merged edge, AND
(iii) the {c_spike, c_side1}-Kempe cycle through the spike contains both
side-1 and the merged edge.
Definitions follow Algorithm 3.1 in paper.tex with G = icosahedron, G' =
dodecahedron, i_1 = 0; the reduced dual has 16 vertices and 24 edges.
Lemmas 2.6 and 2.7 force these properties for ANY proper 3-edge-colouring
of the reduced dual of a *minimal counterexample's* dual. The dodecahedron
is the dual of the icosahedron --- which IS 4-colourable, so the lemmas do
not apply and the question is non-trivial.
"""
from sage.all import Graph
def build_reduced_dodecahedron(i_red=0):
edges = []
for k in range(5):
edges.append((('b', k), ('c', k)))
edges.append((('b', k), ('c', (k - 1) % 5)))
edges.append((('c', k), ('d', k)))
edges.append((('d', k), ('d', (k + 1) % 5)))
v_n = 'v_n'
edges.append((v_n, ('b', i_red % 5)))
edges.append((v_n, ('b', (i_red + 1) % 5)))
edges.append((v_n, ('b', (i_red + 2) % 5)))
edges.append((('b', (i_red + 3) % 5), ('b', (i_red + 4) % 5)))
return Graph(edges, multiedges=False, loops=False)
def enumerate_proper_3_edge_colorings(G):
"""Return (edge_list, list_of_colorings) where each coloring is a list of
3-edge-colours indexed by edge_list."""
edges = list(G.edges(labels=False))
n = len(edges)
adj = [[] for _ in range(n)]
for i in range(n):
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
results = []
def back(k):
if k == n:
results.append(coloring[:])
return
for c in range(3):
if all(coloring[j] != c for j in adj[k]):
coloring[k] = c
back(k + 1)
coloring[k] = -1
back(0)
return edges, results
def kempe_cycle_edges(edges, coloring, start_edge_idx, color_pair):
"""Return the set of edge-indices in the Kempe cycle containing
edges[start_edge_idx] in the subgraph of edges with colour in
color_pair."""
a, b = color_pair
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
if start_edge_idx not in in_sub:
return None
visited = {start_edge_idx}
stack = [start_edge_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 main():
G = build_reduced_dodecahedron(i_red=0)
print(f"|V| = {G.order()}, |E| = {G.size()}")
edges, colorings = enumerate_proper_3_edge_colorings(G)
print(f"Proper 3-edge-colorings total: {len(colorings)}")
v_n = 'v_n'
spike_set = frozenset({v_n, ('b', 1)})
side_0_set = frozenset({v_n, ('b', 0)})
side_1_set = frozenset({v_n, ('b', 2)})
merged_set = frozenset({('b', 3), ('b', 4)})
idx = {}
for i, e in enumerate(edges):
s = frozenset((e[0], e[1]))
if s == spike_set: idx['spike'] = i
if s == side_0_set: idx['side_0'] = i
if s == side_1_set: idx['side_1'] = i
if s == merged_set: idx['merged'] = i
n_chord_apex = 0 # spike == merged
n_kc0_ok = 0 # condition (ii)
n_kc1_ok = 0 # condition (iii)
n_all_ok = 0 # all three
example = None
for col in colorings:
c_spike = col[idx['spike']]
c_merged = col[idx['merged']]
c_s0 = col[idx['side_0']]
c_s1 = col[idx['side_1']]
if c_spike != c_merged:
continue
n_chord_apex += 1
kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
kc0_ok = kc0 is not None and idx['side_0'] in kc0 and idx['merged'] in kc0
kc1_ok = kc1 is not None and idx['side_1'] in kc1 and idx['merged'] in kc1
n_kc0_ok += int(kc0_ok)
n_kc1_ok += int(kc1_ok)
if kc0_ok and kc1_ok:
n_all_ok += 1
if example is None:
example = (col, kc0, kc1)
print()
print(f"Colorings with spike == merged (chord-apex condition): {n_chord_apex}")
print(f" ... + {{c, c_0}}-Kempe cycle through spike/side-0/merged: {n_kc0_ok}")
print(f" ... + {{c, c_1}}-Kempe cycle through spike/side-1/merged: {n_kc1_ok}")
print(f" ... ALL THREE conditions: {n_all_ok}")
if example is not None:
col, kc0, kc1 = example
c_spike = col[idx['spike']]
print()
print("Example coloring (one of {} matching):".format(n_all_ok))
for role in ('spike', 'side_0', 'side_1', 'merged'):
print(f" {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
print(f" spike colour = merged colour = {c_spike}")
print(f" Kempe cycle {{c, c_0}} (through spike): {len(kc0)} edges")
for i in sorted(kc0):
print(f" [c={col[i]}] {edges[i]}")
print(f" Kempe cycle {{c, c_1}} (through spike): {len(kc1)} edges")
for i in sorted(kc1):
print(f" [c={col[i]}] {edges[i]}")
else:
# Dissect a sample chord-apex coloring that fails the Kempe condition
print()
print("Sample chord-apex coloring (fails the Kempe-chain condition):")
for col in colorings:
if col[idx['spike']] != col[idx['merged']]:
continue
c_spike = col[idx['spike']]
c_s0 = col[idx['side_0']]
c_s1 = col[idx['side_1']]
for role in ('spike', 'side_0', 'side_1', 'merged'):
print(f" {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
kc_m0 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s0))
kc_m1 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s1))
print(f" spike's {{c, c_0}}-Kempe cycle has {len(kc0)} edges; "
f"side_0 in it = {idx['side_0'] in kc0}, "
f"merged in it = {idx['merged'] in kc0}")
print(f" merged's {{c, c_0}}-Kempe cycle has {len(kc_m0)} edges; "
f"disjoint from spike's? {kc0.isdisjoint(kc_m0)}")
print(f" spike's {{c, c_1}}-Kempe cycle has {len(kc1)} edges; "
f"side_1 in it = {idx['side_1'] in kc1}, "
f"merged in it = {idx['merged'] in kc1}")
print(f" merged's {{c, c_1}}-Kempe cycle has {len(kc_m1)} edges; "
f"disjoint from spike's? {kc1.isdisjoint(kc_m1)}")
break
if __name__ == '__main__':
main()
@@ -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,238 @@
"""Iterated reduced-dual reduction with protected edges (Sage).
Implements Algorithm 3.1 from paper.tex:
0. G' = dual of G (a triangulation conjectured to be a minimal
counterexample).
1. Apply Definition 2.1 to G' at a pentagonal face to get H_1; find a
proper 3-edge-colouring phi_1 of H_1.
2. Initialise protected edge set E := {spike, side-0, side-1, merged} from
step 1.
3. Iterate: find a pentagonal face of H_{t-1} whose 10 incident edges are
disjoint from E, pick a valid index i_t (Lemma 2.4), apply
Definition 2.1, extend phi_{t-1} to phi_t, and add the four new named
edges to E.
4. Terminate when no safe pentagonal face exists.
We run on the icosahedron-dodecahedron pair as a concrete trace; the
icosahedron is 4-colourable, so the dodecahedron is 3-edge-colourable and
the algorithm terminates combinatorially.
Run with:
sage experiments/iterated_reduction.py
"""
from sage.all import Graph
from sage.graphs.graph_coloring import edge_coloring
# ---------------------------------------------------------------------------
# Build G' = dodecahedron with the same naming as experiments/reduced_dual.py:
# vertex families a (inner pentagon, will play the role of partial F_v's
# boundary vertices for the first reduction), b, c, d.
# ---------------------------------------------------------------------------
def build_dodecahedron():
edges = []
for i in range(5):
edges.append((('a', i), ('a', (i + 1) % 5))) # inner pentagon
edges.append((('a', i), ('b', i))) # spoke a-b
edges.append((('b', i), ('c', i))) # b-c
edges.append((('b', i), ('c', (i - 1) % 5))) # b-c (other side)
edges.append((('c', i), ('d', i))) # spoke c-d
edges.append((('d', i), ('d', (i + 1) % 5))) # outer pentagon
G = Graph(edges, multiedges=False, loops=False)
assert G.is_planar(set_embedding=True), "dodecahedron not planar?"
return G
# ---------------------------------------------------------------------------
# 3-edge-colour a cubic plane graph; return None if not 3-edge-colourable.
# ---------------------------------------------------------------------------
def proper_3_coloring(G):
classes = edge_coloring(G, value_only=False)
if len(classes) > 3:
return None
out = {}
for k, edge_list in enumerate(classes):
for u, v in edge_list:
out[frozenset([u, v])] = k
return out
# ---------------------------------------------------------------------------
# Face search: pentagonal face whose 10 incident edges are outside `protected`.
# Returns (boundary, externals, A) or None.
# ---------------------------------------------------------------------------
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]
# the external edge at each boundary vertex (the one not on the 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:
# In a cubic graph each face-boundary vertex has exactly one
# external edge; otherwise something is off.
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
# ---------------------------------------------------------------------------
# Lemma 2.4: any proper 3-edge-colouring forces the external vector at a
# pentagonal face to have shape (a, b, c, c, c) up to cyclic rotation. The
# valid index i for the reduction is one where positions i+3, i+4 host the
# doubled colour and positions i, i+1, i+2 host three distinct colours.
# ---------------------------------------------------------------------------
def valid_indices(f_vec):
out = []
for i in range(5):
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
continue
triple = {f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}
if len(triple) == 3:
out.append(i)
return out
# ---------------------------------------------------------------------------
# Apply Definition 2.1 at (F, i): delete the 5 boundary vertices, add v_n
# connected to A[i], A[i+1], A[i+2], add the chord A[i+3]-A[i+4]. Extend the
# colouring: every surviving edge keeps its colour; each new edge takes the
# colour of the deleted external at the same A_k (the unique third colour at
# A_k under the surviving edges).
# ---------------------------------------------------------------------------
def apply_reduction(G, boundary, externals, A, coloring, i, v_n_label):
G_new = G.copy()
for v in boundary:
G_new.delete_vertex(v)
G_new.add_vertex(v_n_label)
side_0 = (v_n_label, A[i])
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])
G_new.add_edges([side_0, spike, side_1, merged])
assert G_new.is_planar(set_embedding=True), "reduction broke planarity"
coloring_new = {
e: c for e, c in coloring.items() if not any(u in boundary for u in e)
}
coloring_new[frozenset(side_0)] = coloring[externals[i]]
coloring_new[frozenset(spike)] = coloring[externals[(i + 1) % 5]]
coloring_new[frozenset(side_1)] = coloring[externals[(i + 2) % 5]]
coloring_new[frozenset(merged)] = coloring[externals[(i + 3) % 5]]
named = {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
}
return G_new, coloring_new, named
# ---------------------------------------------------------------------------
# Sanity check: verify that a coloring is proper (each vertex sees 3 colours).
# ---------------------------------------------------------------------------
def is_proper_3_coloring(G, coloring):
for v in G.vertex_iterator():
seen = set()
for u in G.neighbor_iterator(v):
seen.add(coloring[frozenset([u, v])])
if len(seen) != G.degree(v):
return False
return True
# ---------------------------------------------------------------------------
# Main driver.
# ---------------------------------------------------------------------------
def run_algorithm(max_iterations=50, verbose=True):
if verbose:
print("STEP 0: G' = dodecahedron (dual of the icosahedron)")
G_prime = build_dodecahedron()
if verbose:
print(f" |V(G')| = {G_prime.order()}, |E(G')| = {G_prime.size()}")
# ---- STEP 1: apply Definition 2.1 to G' at the inner pentagon, i_1 = 0
face_data = find_safe_pentagonal_face(G_prime, set())
if face_data is None:
print(" No pentagonal face in G'.")
return
boundary, externals, A = face_data
H = G_prime.copy()
for v in boundary:
H.delete_vertex(v)
v_n_label = ('v_n', 1)
H.add_vertex(v_n_label)
side_0 = (v_n_label, A[0])
spike = (v_n_label, A[1])
side_1 = (v_n_label, A[2])
merged = (A[3], A[4])
H.add_edges([side_0, spike, side_1, merged])
assert H.is_planar(set_embedding=True)
coloring = proper_3_coloring(H)
if coloring is None:
print(" H_1 is not 3-edge-colourable; algorithm halts.")
return
assert is_proper_3_coloring(H, coloring)
if verbose:
print(f"STEP 1: H_1 = reduced dual at the first face, i_1 = 0")
print(f" |V(H_1)| = {H.order()}, |E(H_1)| = {H.size()}; "
f"3-edge-coloured.")
# ---- STEP 2: initialise the protected set
E = {
frozenset(spike),
frozenset(side_0),
frozenset(side_1),
frozenset(merged),
}
if verbose:
print(f"STEP 2: |E_protected| = {len(E)}")
# ---- STEP 3-4: iterate
for t in range(2, max_iterations + 1):
face_data = find_safe_pentagonal_face(H, E)
if face_data is None:
if verbose:
print(f"\nTerminated at iteration t = {t}: "
f"no pentagonal face avoids E.")
break
boundary, externals, A = face_data
f_vec = [coloring[e] for e in externals]
choices = valid_indices(f_vec)
if not choices:
print(f" iter t = {t}: f-vector {f_vec} has no valid index "
f"(Lemma 2.4 should preclude this --- bug!)")
break
i_t = choices[0]
v_n_label = ('v_n', t)
H, coloring, named = apply_reduction(
H, boundary, externals, A, coloring, i_t, v_n_label)
assert is_proper_3_coloring(H, coloring)
E |= set(named.values())
if verbose:
print(f" iter t = {t:>2}: f = {f_vec}, chose i_t = {i_t}; "
f"|V| = {H.order():>2}, |E_graph| = {H.size():>2}, "
f"|E_protected| = {len(E):>2}")
else:
if verbose:
print(f"\nReached max_iterations = {max_iterations}.")
if verbose:
print(f"\nFinal H: |V| = {H.order()}, |E| = {H.size()}, "
f"|E_protected| = {len(E)}")
if __name__ == '__main__':
run_algorithm()
@@ -0,0 +1,225 @@
"""Search for a proper 3-edge-coloring of a reduced dual satisfying:
(i) color(spike) == color(merged)
(ii) the {c_spike, c_side_0}-Kempe cycle through the spike contains
side_0 AND merged
(iii) the {c_spike, c_side_1}-Kempe cycle through the spike contains
side_1 AND merged
Iterates over all min-degree-5 simple planar triangulations on n vertices
(via plantri through Sage), then every pentagonal face of the dual G', then
every index i in {0,...,4} for the reduced-dual construction, then every
proper 3-edge-coloring of the resulting reduced dual. Stops on the first hit.
The icosahedron (n = 12) is the only n = 12 triangulation with min degree 5
and was already known to fail (see check_dodecahedron_kempe.py).
Run with:
sage experiments/search_kempe_property.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
import time
def dual_of(G):
"""Combinatorial dual of a planar G with the embedding set on 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):
"""Apply Definition 2.1 at the pentagonal `face` (list of directed edges)
with index i, returning (H, named) or (None, None) if the construction
breaks."""
boundary = [u for (u, v) in face]
if len(set(boundary)) != 5:
return None, 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, None
A.append(outer[0])
if len(set(A)) != 5:
return None, None
H = G.copy()
for v in boundary:
H.delete_vertex(v)
v_n = '__v_n__'
H.add_vertex(v_n)
side_0 = (v_n, A[i % 5])
spike = (v_n, A[(i + 1) % 5])
side_1 = (v_n, A[(i + 2) % 5])
merged = (A[(i + 3) % 5], A[(i + 4) % 5])
# If merged would be a self-loop or duplicate of an existing edge, skip
if A[(i + 3) % 5] == A[(i + 4) % 5]:
return None, None
H.add_edges([side_0, spike, side_1, merged])
if H.has_multiple_edges() or H.has_loops():
return None, None
if not H.is_planar(set_embedding=True):
return None, None
# Cubic check
if not all(H.degree(v) == 3 for v in H.vertex_iterator()):
return None, None
named = {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
}
return H, named
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
if coloring[start_idx] not in (a, b):
return None
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
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(edges, col, named_idx):
c_spike = col[named_idx['spike']]
c_merged = col[named_idx['merged']]
if c_spike != c_merged:
return False
c_s0 = col[named_idx['side_0']]
c_s1 = col[named_idx['side_1']]
kc0 = kempe_cycle(edges, col, named_idx['spike'], (c_spike, c_s0))
if named_idx['side_0'] not in kc0 or named_idx['merged'] not in kc0:
return False
kc1 = kempe_cycle(edges, col, named_idx['spike'], (c_spike, c_s1))
if named_idx['side_1'] not in kc1 or named_idx['merged'] not in kc1:
return False
return True
def report(edges, col, named_idx, named, info):
print()
print("*** MATCH FOUND ***")
for k, v in info.items():
print(f" {k} = {v}")
print("Named edges and their colours:")
for role in ('spike', 'side_0', 'side_1', 'merged'):
e = edges[named_idx[role]]
print(f" {role:7s}: edge {tuple(e)}, colour {col[named_idx[role]]}")
print(f"Full coloring (indexed by edge order):")
for i, e in enumerate(edges):
print(f" [{col[i]}] {tuple(e)}")
def search(max_n=16, time_budget_sec=600):
start = time.time()
n = 12
while n <= max_n:
elapsed = time.time() - start
if elapsed > time_budget_sec:
print(f"\nTime budget {time_budget_sec}s exhausted at n = {n}.")
return
print(f"\n=== n = {n} === (elapsed {elapsed:.1f}s)")
tcount = 0
total_reductions = 0
total_colorings = 0
try:
it = graphs.triangulations(n, minimum_degree=5)
except Exception as ex:
print(f" cannot enumerate triangulations: {ex}")
n += 1
continue
for G in it:
tcount += 1
if not G.is_planar(set_embedding=True):
continue
D = dual_of(G)
if not D.is_planar(set_embedding=True):
continue
faces_D = D.faces()
pentagonal = [f for f in faces_D if len(f) == 5]
for face in pentagonal:
for i_red in range(5):
H, named = apply_reduction(D, face, i_red)
if H is None:
continue
total_reductions += 1
edges, gen = proper_3_edge_colorings(H)
# Find indices of named edges
named_idx = {}
for ii, e in enumerate(edges):
es = frozenset((e[0], e[1]))
for role, ns in named.items():
if es == ns:
named_idx[role] = ii
if len(named_idx) != 4:
continue
for col in gen:
total_colorings += 1
if matches(edges, col, named_idx):
report(edges, col, named_idx, named, {
'n': n,
'triangulation_index': tcount,
'face_size': len(face),
'i_red': i_red,
'reduced_dual_V': H.order(),
'reduced_dual_E': H.size(),
})
return
sys.stdout.flush()
print(f" n = {n}: {tcount} triangulation(s), "
f"{total_reductions} reductions, "
f"{total_colorings} colorings; no hit.")
n += 1
print(f"\nSearch exhausted up to n = {max_n}.")
if __name__ == '__main__':
search()
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

@@ -13,9 +13,15 @@
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces The proof of Lemma\nonbreakingspace 2.6\hbox {}, illustrated for $i = 0$ on $G' = $ the dodecahedron. Top: under the assumption $W \neq Y$, propriety at $v_n$ forces $W \in \{X, Z\}$. Bottom: in either case the lift to $G'$ has externals satisfying the hypothesis of Lemma\nonbreakingspace 2.4\hbox {}, which colours $\partial F_v$ to extend $\psi $ to a proper $3$-edge-colouring of $G'$.}}{5}{}\protected@file@percent }
\newlabel{fig:chord-apex-proof}{{2}{5}}
\newlabel{lem:kempe-spike}{{2.7}{6}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{An iterated reduction}}{7}{}\protected@file@percent }
\newlabel{alg:iterated-reduction}{{3.1}{7}}
\newlabel{rem:alg-invariants}{{3.2}{7}}
\newlabel{rem:alg-chord-apex}{{3.3}{7}}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{0pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\gdef \@abspage@last{7}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Algorithm\nonbreakingspace 3.1\hbox {} on $G' = $ dodecahedron (dual of the icosahedron). \emph {Left:} $G'$ (20 vertices, 30 edges), with $F_v$ (the inner pentagon) shaded as the face chosen for the first reduction. \emph {Centre:} $H_1$ (16 vertices, 24 edges) after step\nonbreakingspace (1) with $i_1 = 0$, $3$-edge-coloured by Sage; the four edges around $v_n^{(1)}$ in $E$ are drawn thicker. \emph {Right:} $H_2$ (12 vertices, 18 edges) after step\nonbreakingspace (3) with $i_t = 0$; the only safe pentagonal face in $H_1$ was the outer pentagon, whose deletion produces $v_n^{(2)}$ and a second chord, giving eight protected edges. No safe pentagonal face remains, so the algorithm terminates. The generating script is \texttt {experiments/draw\_iterated\_reduction.py}.}}{8}{}\protected@file@percent }
\newlabel{fig:iterated-reduction-trace}{{3}{8}}
\gdef \@abspage@last{8}
@@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 23 MAY 2026 02:40
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 23 MAY 2026 03:21
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@@ -144,26 +144,26 @@ File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
(./paper.aux)
\openout1 = `paper.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 25.
LaTeX Font Info: ... okay on input line 25.
LaTeX Font Info: Trying to load font information for U+msa on input line 25.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 26.
LaTeX Font Info: ... okay on input line 26.
LaTeX Font Info: Trying to load font information for U+msa on input line 26.
(/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 25.
LaTeX Font Info: Trying to load font information for U+msb on input line 26.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
@@ -192,7 +192,7 @@ 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}]
Overfull \hbox (41.917pt too wide) in paragraph at lines 143--145
Overfull \hbox (41.917pt too wide) in paragraph at lines 144--146
[]\OT1/cmr/m/n/10 List the five degree-$2$ ver-tices in clock-wise or-der aroun
d $\OML/cmm/m/it/10 F$ \OT1/cmr/m/n/10 as $\OML/cmm/m/it/10 A \OT1/cmr/m/n/10 =
(\OML/cmm/m/it/10 A[]; A[]; A[]; A[]; A[]\OT1/cmr/m/n/10 )$.
@@ -201,22 +201,22 @@ d $\OML/cmm/m/it/10 F$ \OT1/cmr/m/n/10 as $\OML/cmm/m/it/10 A \OT1/cmr/m/n/10 =
<fig_reduced_dual_step1.png, id=17, 517.79329pt x 499.08812pt>
File: fig_reduced_dual_step1.png Graphic file (type png)
<use fig_reduced_dual_step1.png>
Package pdftex.def Info: fig_reduced_dual_step1.png used on input line 163.
Package pdftex.def Info: fig_reduced_dual_step1.png used on input line 164.
(pdftex.def) Requested size: 172.79846pt x 166.55775pt.
<fig_reduced_dual_step2.png, id=19, 490.16064pt x 483.35876pt>
File: fig_reduced_dual_step2.png Graphic file (type png)
<use fig_reduced_dual_step2.png>
Package pdftex.def Info: fig_reduced_dual_step2.png used on input line 164.
Package pdftex.def Info: fig_reduced_dual_step2.png used on input line 165.
(pdftex.def) Requested size: 172.79846pt x 170.39505pt.
<fig_reduced_dual_step3.png, id=20, 490.16064pt x 483.35876pt>
File: fig_reduced_dual_step3.png Graphic file (type png)
<use fig_reduced_dual_step3.png>
Package pdftex.def Info: fig_reduced_dual_step3.png used on input line 165.
Package pdftex.def Info: fig_reduced_dual_step3.png used on input line 166.
(pdftex.def) Requested size: 172.79846pt x 170.39505pt.
<fig_reduced_dual_step4.png, id=21, 490.16064pt x 486.3346pt>
File: fig_reduced_dual_step4.png Graphic file (type png)
<use fig_reduced_dual_step4.png>
Package pdftex.def Info: fig_reduced_dual_step4.png used on input line 166.
Package pdftex.def Info: fig_reduced_dual_step4.png used on input line 167.
(pdftex.def) Requested size: 172.79846pt x 171.44409pt.
LaTeX Warning: `h' float specifier changed to `ht'.
@@ -226,50 +226,78 @@ uced_dual_step3.png> <./fig_reduced_dual_step4.png>]
<fig_chord_apex_step1.png, id=34, 505.03976pt x 502.06393pt>
File: fig_chord_apex_step1.png Graphic file (type png)
<use fig_chord_apex_step1.png>
Package pdftex.def Info: fig_chord_apex_step1.png used on input line 287.
Package pdftex.def Info: fig_chord_apex_step1.png used on input line 288.
(pdftex.def) Requested size: 251.9989pt x 250.5104pt.
<fig_chord_apex_step2.png, id=35, 490.16064pt x 499.51323pt>
File: fig_chord_apex_step2.png Graphic file (type png)
<use fig_chord_apex_step2.png>
Package pdftex.def Info: fig_chord_apex_step2.png used on input line 288.
Package pdftex.def Info: fig_chord_apex_step2.png used on input line 289.
(pdftex.def) Requested size: 172.79846pt x 176.08986pt.
<fig_chord_apex_step3.png, id=36, 490.16064pt x 499.51323pt>
File: fig_chord_apex_step3.png Graphic file (type png)
<use fig_chord_apex_step3.png>
Package pdftex.def Info: fig_chord_apex_step3.png used on input line 289.
Package pdftex.def Info: fig_chord_apex_step3.png used on input line 290.
(pdftex.def) Requested size: 172.79846pt x 176.08986pt.
LaTeX Warning: `h' float specifier changed to `ht'.
[4] [5 <./fig_chord_apex_step1.png> <./fig_chord_apex_step2.png> <./fig_chord_a
pex_step3.png>] [6] [7] (./paper.aux) )
Here is how much of TeX's memory you used:
3043 strings out of 478268
43156 string characters out of 5846347
343146 words of memory out of 5000000
21084 multiletter control sequences out of 15000+600000
476364 words of font info for 55 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
69i,8n,76p,664b,298s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/
type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
ype1/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pu
blic/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
c/amsfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
/amsfonts/cm/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/symbols/msam10.pfb>
Output written on paper.pdf (7 pages, 755306 bytes).
PDF statistics:
108 PDF objects out of 1000 (max. 8388607)
58 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
36 words of extra memory for PDF output out of 10000 (max. 10000000)
pex_step3.png>] [6]
Overfull \hbox (4.76643pt too wide) in paragraph at lines 432--439
\OT1/cmr/m/n/10 which $\OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[
]\OT1/cmr/m/n/10 ) = \OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\
OT1/cmr/m/n/10 )$ and $\OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[
]\OT1/cmr/m/n/10 )\OML/cmm/m/it/10 ; '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\O
T1/cmr/m/n/10 )\OML/cmm/m/it/10 ; '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/
cmr/m/n/10 )$
[]
<fig_alg_step0.png, id=49, 462.528pt x 440.42188pt>
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 478.
(pdftex.def) Requested size: 115.20264pt x 109.69525pt.
<fig_alg_step1.png, id=50, 462.528pt x 440.42188pt>
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 479.
(pdftex.def) Requested size: 115.20264pt x 109.69525pt.
<fig_alg_step2.png, id=51, 480.80806pt x 371.1277pt>
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 480.
(pdftex.def) Requested size: 115.20264pt x 88.91956pt.
[7] [8 <./fig_alg_step0.png> <./fig_alg_step1.png> <./fig_alg_step2.png>]
(./paper.aux) )
Here is how much of TeX's memory you used:
3075 strings out of 478268
43781 string characters out of 5846347
343216 words of memory out of 5000000
21112 multiletter control sequences out of 15000+600000
476532 words of font info for 56 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
69i,8n,76p,918b,298s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/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/cm/cm
mi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr1
0.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.p
fb></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></u
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb></us
r/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr/
local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/l
ocal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/loc
al/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/loca
l/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (8 pages, 983037 bytes).
PDF statistics:
132 PDF objects out of 1000 (max. 8388607)
69 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
51 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -16,6 +16,7 @@
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\newtheorem{algorithm}[theorem]{Algorithm}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
@@ -391,4 +392,103 @@ distinct colours, contradicting Lemma~\ref{lem:chord-apex} applied to
$\varphi'$.
\end{proof}
\section{An iterated reduction}
The reduced-dual construction in Definition~\ref{def:reduced-dual} 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
Definition~\ref{def:reduced-dual} 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 Lemma~\ref{lem:pentagonal-externals} 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 Definition~\ref{def:reduced-dual} 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 Lemma~\ref{lem:pentagonal-externals}, 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}
Lemma~\ref{lem:chord-apex} 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 Lemma~\ref{lem:chord-apex}
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' = $ dodecahedron
(dual of the icosahedron). \emph{Left:} $G'$ (20 vertices, 30 edges), with
$F_v$ (the inner pentagon) shaded as the face chosen for the first reduction.
\emph{Centre:} $H_1$ (16 vertices, 24 edges) after step~(1) with $i_1 = 0$,
$3$-edge-coloured by Sage; the four edges around $v_n^{(1)}$ in $E$ are drawn
thicker. \emph{Right:} $H_2$ (12 vertices, 18 edges) after step~(3) with
$i_t = 0$; the only safe pentagonal face in $H_1$ was the outer pentagon,
whose deletion produces $v_n^{(2)}$ and a second chord, giving eight protected
edges. No safe pentagonal face remains, so the algorithm terminates. The
generating script is \texttt{experiments/draw\_iterated\_reduction.py}.}
\label{fig:iterated-reduction-trace}
\end{figure}
\end{document}