dual_decomposition: Conj 3.6 (face/Kempe witness) and constructive lift

Paper:
- Lemmas 3.4 (exactly one match) and 3.5 (all-distinct exists for 4-colourable
  G) replace the earlier conjecture; both have proofs.
- Add Conjecture 3.6: every proper 3-edge-colouring of a counterexample's
  reduced dual has a face with two same-colour edges that share a Kempe
  cycle with the merged edge, neither of them being the merged edge.

Experiments (all under experiments/):
- search_conj_3_6_counterexample.py: finds n=14 tri#1 i_red=0 where the
  algorithm's phi_t* sits in a Kempe class with no all-distinct colouring
  (disproves an earlier formulation).
- check_kempe_class.py / check_kempe_class_invariance.py /
  check_kempe_class_monotone.py: Kempe-class counts on H_1 and H_t* for
  small triangulations; neither monotonicity direction holds.
- check_all_distinct_exists.py: even in the conj-3.6 disproof case, H_t*
  itself admits all-distinct colourings in the *other* Kempe class.
- check_constrained_feasibility.py: literal H_t*-interpretation of
  C1 + K0 + K1 is empirically unsatisfiable (gap in proof strategy noted).
- check_conj_face_kempe.py / check_conj_face_kempe_n15.py: test Conj 3.6
  on chord-apex+Kempe colourings of reduced duals at n=12, 14, 15;
  216/216 colourings on n=14 satisfy the conjecture, others vacuous.
- draw_step1_conj36.py: figure showing a Conj 3.6 witness on H_1 with two
  new vertices on the witness edges and a new red bridge between them.
- draw_step1_conj36_recolored.py: same but with the Kempe cycle recoloured
  alternately from merged so propriety holds.
- draw_lift_to_Gprime.py: lifts the modified+recoloured H_1 back to a
  proper 3-edge-colouring of the modified G' (24+2 vertices, 39 edges,
  same Tutte layout as figure 3's first graphic so positions line up).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 11:27:50 -04:00
parent 03dcd7c2fa
commit 464c524fa1
20 changed files with 3326 additions and 113 deletions
@@ -0,0 +1,329 @@
"""Probe: even when phi_t* is in a Kempe class with no all-distinct, does
H_t* itself admit an all-distinct coloring in SOME (other) Kempe class?
Reuses the Conj 3.6 counterexample: n=14, tri#1, i_red=0, phi_1 from the
chord-apex+Kempe colorings. Enumerates every proper 3-edge-coloring of
H_t*, marks the Kempe classes, and for each class reports whether it
contains an all-distinct coloring.
Run with: sage experiments/check_all_distinct_exists.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
return Graph(dual_edges, multiedges=False, loops=False)
def proper_3_edge_colorings(G):
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(tuple(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, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b):
return set()
in_sub = set(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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
def matches_chord_apex_kempe(edges, col, named):
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()):
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 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])
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
return {
'H': H, 'A': A,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
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 = []
ok = True
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
ok = False
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
if not ok:
continue
if any(e in protected for e in boundary_edges + externals):
continue
return boundary, externals, A
return None
def valid_index(f_vec, A):
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
return i
return None
def run_algorithm_to_completion(H, phi_1_dict, named_step1, vn_start=10000):
H = H.copy()
coloring = dict(phi_1_dict)
all_named = [named_step1]
E = set(named_step1.values())
next_vn = vn_start
while True:
H.is_planar(set_embedding=True)
result = find_safe_pentagonal_face(H, E)
if result is None:
break
boundary, externals, A = result
f_vec = [coloring[e] for e in externals]
i_t = valid_index(f_vec, A)
if i_t is None:
break
v_n = next_vn
next_vn += 1
H_new = H.copy()
for v in boundary:
H_new.delete_vertex(v)
H_new.add_vertex(v_n)
side_0 = (v_n, A[i_t])
spike = (v_n, A[(i_t + 1) % 5])
side_1 = (v_n, 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])
if H_new.has_multiple_edges() or H_new.has_loops():
break
if not H_new.is_planar(set_embedding=True):
break
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)] = 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())
all_named.append(named)
return H, coloring, all_named
def is_all_distinct(edges, col, all_named):
for named in all_named:
i_s = edge_idx(edges, named['spike'])
i_m = edge_idx(edges, named['merged'])
if i_s is None or i_m is None:
return False
if col[i_s] == col[i_m]:
return False
return True
def main():
print("Reconstructing Conj 3.6 counterexample: n=14, tri#1, i_red=0")
for G in graphs.triangulations(14, minimum_degree=5):
D = dual_of(G)
D.is_planar(set_embedding=True)
chosen = None
for face in D.faces():
if len(face) != 5:
continue
res = apply_reduction(D, face, 0, 9999)
if res is None:
continue
H_1, named_1 = res['H'], res['named']
edges_1, colorings_1 = proper_3_edge_colorings(H_1)
cand = [c for c in colorings_1
if matches_chord_apex_kempe(edges_1, c, named_1)]
if cand:
chosen = (face, res, cand)
break
if chosen is None:
continue
face, res, cand = chosen
H_1, named_1 = res['H'], res['named']
edges_1, _ = proper_3_edge_colorings(H_1)
if not cand:
continue
phi_1 = cand[0]
phi_1_dict = {frozenset((e[0], e[1])): c for e, c in zip(edges_1, phi_1)}
H_final, phi_final_dict, all_named = run_algorithm_to_completion(
H_1, phi_1_dict, named_1)
edges_f, colorings_f = proper_3_edge_colorings(H_final)
print(f" H_t*: |V|={H_final.order()}, |E|={H_final.size()}, "
f"{len(colorings_f)} proper 3-edge-colorings, "
f"{len(all_named)} named-edge steps.")
# Kempe equivalence classes via union-find
col_idx = {c: i for i, c in enumerate(colorings_f)}
parent = list(range(len(colorings_f)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
for c_idx, col in enumerate(colorings_f):
for pair in [(0, 1), (0, 2), (1, 2)]:
visited = set()
for start in range(len(edges_f)):
if col[start] not in pair or start in visited:
continue
cyc = kempe_cycle(edges_f, col, start, pair)
visited |= cyc
a, b = pair
new_col = list(col)
for k in cyc:
if new_col[k] == a:
new_col[k] = b
elif new_col[k] == b:
new_col[k] = a
new_col = tuple(new_col)
if new_col != col and new_col in col_idx:
union(c_idx, col_idx[new_col])
roots = {}
for i in range(len(colorings_f)):
r = find(i)
roots.setdefault(r, []).append(i)
# phi_t* identification
phi_final = tuple(phi_final_dict[frozenset((e[0], e[1]))]
for e in edges_f)
phi_idx = colorings_f.index(phi_final)
phi_root = find(phi_idx)
print()
print(f" Found {len(roots)} Kempe equivalence classes:")
for r, members in sorted(roots.items(), key=lambda kv: -len(kv[1])):
ad = [m for m in members
if is_all_distinct(edges_f, colorings_f[m], all_named)]
label = " (contains phi_t*)" if r == phi_root else ""
print(f" class size {len(members)}: "
f"{len(ad)} all-distinct colorings{label}")
return
if __name__ == '__main__':
main()
@@ -0,0 +1,192 @@
"""Test Conjecture 3.6: for every reduced dual of a minimal counterexample,
every proper 3-edge-coloring has a face with two same-color edges that share
a Kempe cycle with the merged edge.
We can't run on actual counterexamples (none exist by the 4CT). Instead we
test on the structural surrogates: proper 3-edge-colorings of reduced duals
that *satisfy* chord-apex (Lemma 2.6) and the Kempe-cycle condition
(Lemma 2.7). These are the kinds of colorings a counterexample's reduced
dual would be forced to admit.
For each such (G, face, i_red, phi), check whether some face F of H_1 and
some pair of edges (e1, e2) in partial F satisfy:
- phi(e1) = phi(e2)
- e1, e2, and merged all lie on a common {a,b}-Kempe cycle.
If conjecture FAILS for some coloring, we have empirical evidence against.
If every coloring passes, evidence in favour.
Run with: sage experiments/check_conj_face_kempe.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
return Graph(
[(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
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 or 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])
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
return {
'H': H,
'named': {
'spike': frozenset(spike), 'side_0': frozenset(side_0),
'side_1': frozenset(side_1), 'merged': frozenset(merged),
},
}
def proper_3_edge_colorings(G):
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(tuple(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, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b):
return set()
in_sub = set(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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
def matches_chord_apex_kempe(edges, col, named):
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()): 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 conjecture_holds_for(H, edges, col, named):
"""Returns (F, e1, e2, kc) if some face F has two same-color edges (e1, e2)
and both, together with merged, lie on a Kempe cycle kc. Else None."""
merged_idx = edge_idx(edges, named['merged'])
c_merged = col[merged_idx]
# All Kempe cycles through merged (one per color pair (c_merged, c'))
kempe_cycles = []
for c_prime in range(3):
if c_prime == c_merged: continue
kc = kempe_cycle(edges, col, merged_idx, (c_merged, c_prime))
kempe_cycles.append(kc)
for face in H.faces():
face_edge_indices = []
for u, v in face:
ei = edge_idx(edges, frozenset((u, v)))
if ei is not None:
face_edge_indices.append(ei)
for i in range(len(face_edge_indices)):
for j in range(i + 1, len(face_edge_indices)):
e1, e2 = face_edge_indices[i], face_edge_indices[j]
if col[e1] != col[e2]: continue
for kc in kempe_cycles:
if e1 in kc and e2 in kc:
return face, e1, e2, kc
return None
def main():
for n in [12, 14, 15]:
print(f"=== n = {n} ===")
try:
triangulations = list(graphs.triangulations(n, minimum_degree=5))
except Exception as ex:
print(f" cannot enumerate: {ex}"); continue
for tri_idx, G in enumerate(triangulations, start=1):
G.is_planar(set_embedding=True)
D = dual_of(G); D.is_planar(set_embedding=True)
for face_no, face in enumerate(
f for f in D.faces() if len(f) == 5):
for i_red in range(5):
res = apply_reduction(D, face, i_red, 9999)
if res is None: continue
H = res['H']; named = res['named']
H.is_planar(set_embedding=True)
edges, colorings = proper_3_edge_colorings(H)
cand = [c for c in colorings
if matches_chord_apex_kempe(edges, c, named)]
if not cand: continue
n_pass = sum(1 for c in cand
if conjecture_holds_for(H, edges, c, named)
is not None)
n_fail = len(cand) - n_pass
status = "PASSED" if n_fail == 0 else "*** FAILED ***"
print(f" n={n} tri#{tri_idx} face#{face_no} i_red={i_red}: "
f"{len(cand)} chord-apex+Kempe colorings; "
f"{n_pass} satisfy conjecture, {n_fail} fail. "
f"{status}")
if __name__ == '__main__':
main()
@@ -0,0 +1,59 @@
"""Run conjecture 3.6 test specifically on n=15 with progress output."""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
# Reuse helpers from check_conj_face_kempe.py by importing
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/experiments')
from check_conj_face_kempe import (
dual_of, apply_reduction, proper_3_edge_colorings,
kempe_cycle, edge_idx, matches_chord_apex_kempe,
conjecture_holds_for
)
def main():
n = 15
print(f"=== n = {n} ===")
triangulations = list(graphs.triangulations(n, minimum_degree=5))
print(f" Found {len(triangulations)} triangulations.")
total_cand = 0
total_pass = 0
total_fail = 0
for tri_idx, G in enumerate(triangulations, start=1):
G.is_planar(set_embedding=True)
D = dual_of(G); D.is_planar(set_embedding=True)
pentagonal_faces = [f for f in D.faces() if len(f) == 5]
for face_no, face in enumerate(pentagonal_faces):
for i_red in range(5):
res = apply_reduction(D, face, i_red, 9999)
if res is None: continue
H = res['H']; named = res['named']
H.is_planar(set_embedding=True)
edges, colorings = proper_3_edge_colorings(H)
cand = [c for c in colorings
if matches_chord_apex_kempe(edges, c, named)]
if not cand: continue
n_pass = sum(1 for c in cand
if conjecture_holds_for(H, edges, c, named)
is not None)
n_fail = len(cand) - n_pass
total_cand += len(cand)
total_pass += n_pass
total_fail += n_fail
status = "PASSED" if n_fail == 0 else "*** FAILED ***"
print(f" tri#{tri_idx} face#{face_no} i_red={i_red}: "
f"{len(cand)} chord-apex+Kempe; {n_pass} pass, "
f"{n_fail} fail. {status}")
sys.stdout.flush()
if tri_idx % 5 == 0:
print(f" ... finished tri#{tri_idx}/{len(triangulations)}; "
f"cumulative cand={total_cand}, pass={total_pass}, "
f"fail={total_fail}")
sys.stdout.flush()
print(f"\nTotal: {total_cand} chord-apex+Kempe colorings tested; "
f"{total_pass} pass, {total_fail} fail.")
if __name__ == '__main__':
main()
@@ -0,0 +1,259 @@
"""Constrained 3-edge-colouring feasibility on H_t*.
The chord-apex (Lemma 2.6) and Kempe-cycle (Lemma 2.7) lemmas give proven
constraints on proper 3-edge-colourings of H_1 when G is a minimal
counterexample. The step-1 constraints, *interpreted on H_t*, are:
(C1) phi(spike_1) = phi(merged_1) [chord-apex]
(K0) the {phi(spike_1), phi(side_0_1)}-Kempe cycle of H_t* through
spike_1 contains side_0_1 and merged_1
(K1) the {phi(spike_1), phi(side_1_1)}-Kempe cycle of H_t* through
spike_1 contains side_1_1 and merged_1
If G is a minimal counterexample then every proper 3-edge-colouring of H_t*
that *lifts back* to a proper 3-edge-colouring of H_1 must satisfy (C1) +
(K0) + (K1) on H_1; the Kempe-cycle structure on H_t* and H_1 can differ,
but we test the H_t*-interpreted versions here as the natural constraint
set the algorithm propagates.
For each min-deg-5 triangulation G and each (face, i_red) we enumerate
proper 3-edge-colourings of H_t* and count those satisfying these step-1
constraints. If the constrained problem is INFEASIBLE on H_t* for some G
in our test set, that would be a hint that constraints alone can rule out
3-edge-colourability of H_t* -- a potential mechanism for proving smaller
counterexamples exist.
Run with: sage experiments/check_constrained_feasibility.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
from sage.graphs.graph_coloring import edge_coloring
import sys
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
return Graph(
[(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
multiedges=False, loops=False)
def proper_3_edge_colorings(G):
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(tuple(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, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b):
return set()
in_sub = set(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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
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 or 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])
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
return {
'H': H,
'named': {
'spike': frozenset(spike), 'side_0': frozenset(side_0),
'side_1': frozenset(side_1), 'merged': frozenset(merged),
},
}
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, ok = [], [], True
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1: ok = False; break
externals.append(frozenset([B_k, outer[0]])); A.append(outer[0])
if not ok: continue
if any(e in protected for e in boundary_edges + externals): continue
return boundary, externals, A
return None
def valid_index(f_vec, A):
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
return i
return None
def run_to_completion(H, phi_dict, named_step1, vn_start=10000):
H = H.copy()
coloring = dict(phi_dict)
all_named = [named_step1]
E = set(named_step1.values())
next_vn = vn_start
while True:
H.is_planar(set_embedding=True)
result = find_safe_pentagonal_face(H, E)
if result is None: break
boundary, externals, A = result
f_vec = [coloring[e] for e in externals]
i_t = valid_index(f_vec, A)
if i_t is None: break
v_n = next_vn; next_vn += 1
H_new = H.copy()
for v in boundary: H_new.delete_vertex(v)
H_new.add_vertex(v_n)
s0 = (v_n, A[i_t]); sp = (v_n, A[(i_t+1) % 5])
s1 = (v_n, A[(i_t+2) % 5]); mg = (A[(i_t+3) % 5], A[(i_t+4) % 5])
H_new.add_edges([s0, sp, s1, mg])
if H_new.has_multiple_edges() or H_new.has_loops(): break
if not H_new.is_planar(set_embedding=True): break
H = H_new
coloring = {e: c for e, c in coloring.items()
if not any(u in boundary for u in e)}
coloring[frozenset(s0)] = f_vec[i_t]
coloring[frozenset(sp)] = f_vec[(i_t+1) % 5]
coloring[frozenset(s1)] = f_vec[(i_t+2) % 5]
coloring[frozenset(mg)] = f_vec[(i_t+3) % 5]
all_named.append({
'spike': frozenset(sp), 'side_0': frozenset(s0),
'side_1': frozenset(s1), 'merged': frozenset(mg),
})
E |= set(all_named[-1].values())
return H, coloring, all_named
def check_constraints(edges, col, named):
"""Check (C1) + (K0) + (K1) on this colouring."""
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()):
return False, False, False
c_spike = col[idx['spike']]
c_merged = col[idx['merged']]
C1 = (c_spike == c_merged)
if not C1:
return False, False, 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))
K0 = idx['side_0'] in kc0 and idx['merged'] in kc0
K1 = idx['side_1'] in kc1 and idx['merged'] in kc1
return C1, K0, K1
def run_case(G, n, tri_idx):
G.is_planar(set_embedding=True)
D = dual_of(G); D.is_planar(set_embedding=True)
for face_n, face in enumerate([f for f in D.faces() if len(f) == 5]):
for i_red in range(5):
res = apply_reduction(D, face, i_red, 9999)
if res is None: continue
H_1, named_1 = res['H'], res['named']
# Default phi_1: Sage's edge_coloring
try:
classes = edge_coloring(H_1, value_only=False)
except Exception:
continue
if len(classes) > 3: continue
phi_1_dict = {}
for k, edge_list in enumerate(classes):
for u, v in edge_list:
phi_1_dict[frozenset([u, v])] = k
H_final, _, all_named = run_to_completion(H_1, phi_1_dict, named_1)
edges_f, colorings_f = proper_3_edge_colorings(H_final)
n_total = len(colorings_f)
n_c1 = 0; n_c1_k0 = 0; n_c1_k1 = 0; n_all = 0
for col in colorings_f:
C1, K0, K1 = check_constraints(edges_f, col, named_1)
if C1: n_c1 += 1
if C1 and K0: n_c1_k0 += 1
if C1 and K1: n_c1_k1 += 1
if C1 and K0 and K1: n_all += 1
feasible = n_all > 0
print(f" n={n} tri#{tri_idx} face#{face_n} i_red={i_red}: "
f"|V(H_t*)|={H_final.order()}, |E|={H_final.size()}, "
f"colorings={n_total}, "
f"C1={n_c1}, C1+K0={n_c1_k0}, C1+K1={n_c1_k1}, "
f"C1+K0+K1={n_all} "
f"{'OK' if feasible else '*** INFEASIBLE ***'}")
sys.stdout.flush()
return # one (face, i_red) per triangulation
def main(max_n=15):
for n in range(12, max_n + 1):
print(f"\n=== n = {n} ===")
try:
triangulations = list(graphs.triangulations(n, minimum_degree=5))
except Exception as ex:
print(f" cannot enumerate: {ex}"); continue
for tri_idx, G in enumerate(triangulations, start=1):
run_case(G, n, tri_idx)
if __name__ == '__main__':
main()
@@ -0,0 +1,183 @@
"""Check empirically whether all proper 3-edge-colorings of a cubic plane
graph form a single Kempe equivalence class.
For each test graph:
1. enumerate all proper 3-edge-colorings;
2. build a multigraph K whose vertices are the colorings and whose edges
connect pairs related by a single Kempe swap (swapping two colors on
one cycle of the 2-color subgraph);
3. report the number of connected components of K (= number of Kempe
classes).
Test inputs:
- Dodecahedron's reduced dual (16 v, 24 e), built as in
check_dodecahedron_kempe.py;
- The n=14 reduced dual found by search_kempe_property.py (20 v, 30 e).
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
def all_proper_3_edge_colorings(G):
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(tuple(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_cycles_of(edges, coloring, color_pair):
"""List of connected components (as sorted tuples of edge indices) in
the subgraph of edges with color in color_pair."""
a, b = color_pair
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
in_set = set(in_sub)
visited = set()
components = []
for start in in_sub:
if start in visited:
continue
comp = []
stack = [start]
visited.add(start)
while stack:
cur = stack.pop()
comp.append(cur)
u, v = edges[cur][0], edges[cur][1]
for j in in_set:
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)
components.append(tuple(sorted(comp)))
return components
def swap_on_cycle(coloring, cycle_edges, color_pair):
a, b = color_pair
swap = list(coloring)
for i in cycle_edges:
if swap[i] == a:
swap[i] = b
elif swap[i] == b:
swap[i] = a
return tuple(swap)
def kempe_components(G):
edges, colorings = all_proper_3_edge_colorings(G)
print(f" {len(colorings)} proper 3-edge-colorings")
idx = {c: i for i, c in enumerate(colorings)}
parent = list(range(len(colorings)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
for col in colorings:
for a, b in [(0, 1), (0, 2), (1, 2)]:
for cyc in kempe_cycles_of(edges, col, (a, b)):
new_col = swap_on_cycle(col, cyc, (a, b))
if new_col == col:
continue
if new_col in idx:
union(idx[col], idx[new_col])
n_classes = len({find(i) for i in range(len(colorings))})
print(f" Kempe equivalence classes: {n_classes}")
return n_classes
def dodecahedron_reduced_dual():
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)))
edges.append(('v_n', ('b', 0)))
edges.append(('v_n', ('b', 1)))
edges.append(('v_n', ('b', 2)))
edges.append((('b', 3), ('b', 4)))
return Graph(edges, multiedges=False, loops=False)
def n14_reduced_dual():
# First min-degree-5 plantri triangulation on 14 vertices, reduced at the
# first pentagonal face with i_red = 1 (matches search_kempe_property.py).
G = next(graphs.triangulations(14, minimum_degree=5))
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
D = Graph(dual_edges, multiedges=False, loops=False)
D.is_planar(set_embedding=True)
# use first pentagonal face, i_red = 1
face = next(f for f in D.faces() if len(f) == 5)
boundary = [u for (u, v) in face]
A = []
for B_k in boundary:
outer = [w for w in D.neighbor_iterator(B_k) if w not in boundary]
A.append(outer[0])
H = D.copy()
for v in boundary:
H.delete_vertex(v)
vn = '__v_n__'
H.add_vertex(vn)
H.add_edge(vn, A[1]) # spike
H.add_edge(vn, A[0]) # side_0
H.add_edge(vn, A[2]) # side_1
H.add_edge(A[3], A[4]) # merged
H.is_planar(set_embedding=True)
return H
def main():
print("Dodecahedron's reduced dual:")
G1 = dodecahedron_reduced_dual()
kempe_components(G1)
print()
print("n=14 reduced dual (first plantri triangulation, i_red=1):")
G2 = n14_reduced_dual()
kempe_components(G2)
print()
print("Small sanity check: K_4")
K4 = graphs.CompleteGraph(4)
kempe_components(K4)
if __name__ == '__main__':
main()
@@ -0,0 +1,288 @@
"""Investigate the conjecture: H_t* has the same number of Kempe equivalence
classes as H_1.
For each min-deg-5 triangulation G (up to some n) and each reduction index
i_red in {0,...,4}:
- Build H_1 from the first pentagonal face of G' = dual(G).
- Count Kempe classes of H_1.
- Run Algorithm 3.1 to completion with Sage's edge_coloring as phi_1.
- Count Kempe classes of H_t*.
- Print a row.
Run with: sage experiments/check_kempe_class_invariance.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
from sage.graphs.graph_coloring import edge_coloring
import sys
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
return Graph(dual_edges, multiedges=False, loops=False)
def proper_3_edge_colorings(G):
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(tuple(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, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b):
return set()
in_sub = set(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 num_kempe_classes(G):
"""Count the Kempe equivalence classes of proper 3-edge-colorings of G."""
edges, colorings = proper_3_edge_colorings(G)
if not colorings:
return None, 0
n = len(colorings)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
col_idx = {c: i for i, c in enumerate(colorings)}
for c_idx, col in enumerate(colorings):
for pair in [(0, 1), (0, 2), (1, 2)]:
visited = set()
for start in range(len(edges)):
if col[start] not in pair or start in visited:
continue
cyc = kempe_cycle(edges, col, start, pair)
visited |= cyc
a, b = pair
new_col = list(col)
for k in cyc:
if new_col[k] == a:
new_col[k] = b
elif new_col[k] == b:
new_col[k] = a
new_col = tuple(new_col)
if new_col != col and new_col in col_idx:
union(c_idx, col_idx[new_col])
classes = len({find(i) for i in range(n)})
return n, classes
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])
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
return {
'H': H, 'A': A,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
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 = []
ok = True
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
ok = False
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
if not ok:
continue
if any(e in protected for e in boundary_edges + externals):
continue
return boundary, externals, A
return None
def valid_index(f_vec, A):
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
return i
return None
def run_algorithm_to_completion(H, phi_1_coloring_dict, named_step1,
vn_start=10000):
H = H.copy()
coloring = dict(phi_1_coloring_dict)
E = set(named_step1.values())
next_vn = vn_start
while True:
H.is_planar(set_embedding=True)
result = find_safe_pentagonal_face(H, E)
if result is None:
break
boundary, externals, A = result
f_vec = [coloring[e] for e in externals]
i_t = valid_index(f_vec, A)
if i_t is None:
break
v_n = next_vn
next_vn += 1
H_new = H.copy()
for v in boundary:
H_new.delete_vertex(v)
H_new.add_vertex(v_n)
side_0 = (v_n, A[i_t])
spike = (v_n, A[(i_t + 1) % 5])
side_1 = (v_n, 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])
if H_new.has_multiple_edges() or H_new.has_loops():
break
if not H_new.is_planar(set_embedding=True):
break
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)] = 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]
E |= {frozenset(spike), frozenset(side_0), frozenset(side_1),
frozenset(merged)}
return H, coloring
def main():
print(f"{'n':>3} {'tri#':>5} {'i_red':>5} | "
f"{'|V(H_1)|':>9} {'|E(H_1)|':>9} {'#col(H_1)':>10} {'#K(H_1)':>8} | "
f"{'|V(H_t*)|':>10} {'|E(H_t*)|':>10} {'#col(H_t*)':>11} {'#K(H_t*)':>10}")
print("-" * 120)
for n in [12, 14]:
try:
triangulations = list(graphs.triangulations(n, minimum_degree=5))
except Exception as ex:
print(f" cannot enumerate n={n}: {ex}")
continue
for tri_idx, G in enumerate(triangulations, start=1):
G_copy = G.copy()
G_copy.is_planar(set_embedding=True)
D = dual_of(G_copy)
D.is_planar(set_embedding=True)
# iterate over all pentagonal faces (or just the first)
faces = [f for f in D.faces() if len(f) == 5]
if not faces:
continue
face = faces[0]
for i_red in range(5):
res = apply_reduction(D, face, i_red, 9999)
if res is None:
continue
H_1 = res['H']
# count Kempe classes of H_1
n_col_h1, n_kc_h1 = num_kempe_classes(H_1)
# run algorithm to completion with Sage's edge_coloring as phi_1
classes = edge_coloring(H_1, value_only=False)
if len(classes) > 3:
continue
phi_1_dict = {}
for k, edge_list in enumerate(classes):
for u, v in edge_list:
phi_1_dict[frozenset([u, v])] = k
H_final, _ = run_algorithm_to_completion(H_1, phi_1_dict,
res['named'])
n_col_hf, n_kc_hf = num_kempe_classes(H_final)
same = "same" if n_kc_h1 == n_kc_hf else "DIFFERENT"
print(f"{n:>3} {tri_idx:>5} {i_red:>5} | "
f"{H_1.order():>9} {H_1.size():>9} {n_col_h1:>10} {n_kc_h1:>8} | "
f"{H_final.order():>10} {H_final.size():>10} "
f"{n_col_hf:>11} {n_kc_hf:>10} {same}")
sys.stdout.flush()
if __name__ == '__main__':
main()
@@ -0,0 +1,310 @@
"""Investigate the conjecture:
#(Kempe classes of H_t*) <= #(Kempe classes of H_1).
For each min-deg-5 triangulation G and each (face, i_red) choice giving a
valid H_1, vary phi_1 across many proper 3-edge-colorings of H_1 -- so we
exercise multiple algorithm executions, each potentially producing a
different H_t*. For each execution, count Kempe classes of H_t* and compare
to H_1.
Reports any case where #classes(H_t*) > #classes(H_1) (counterexample to
the conjecture).
Run with: sage experiments/check_kempe_class_monotone.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
import time
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
return Graph(dual_edges, multiedges=False, loops=False)
def proper_3_edge_colorings(G):
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(tuple(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, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b):
return set()
in_sub = set(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 num_kempe_classes(G):
edges, colorings = proper_3_edge_colorings(G)
if not colorings:
return 0, 0
n = len(colorings)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
col_idx = {c: i for i, c in enumerate(colorings)}
for c_idx, col in enumerate(colorings):
for pair in [(0, 1), (0, 2), (1, 2)]:
visited = set()
for start in range(len(edges)):
if col[start] not in pair or start in visited:
continue
cyc = kempe_cycle(edges, col, start, pair)
visited |= cyc
a, b = pair
new_col = list(col)
for k in cyc:
if new_col[k] == a:
new_col[k] = b
elif new_col[k] == b:
new_col[k] = a
new_col = tuple(new_col)
if new_col != col and new_col in col_idx:
union(c_idx, col_idx[new_col])
classes = len({find(i) for i in range(n)})
return n, classes
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])
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
return {
'H': H, 'A': A,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
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 = []
ok = True
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
ok = False
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
if not ok:
continue
if any(e in protected for e in boundary_edges + externals):
continue
return boundary, externals, A
return None
def valid_index(f_vec, A):
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
return i
return None
def run_algorithm_to_completion(H, phi_1_dict, named_step1, vn_start=10000):
H = H.copy()
coloring = dict(phi_1_dict)
E = set(named_step1.values())
next_vn = vn_start
while True:
H.is_planar(set_embedding=True)
result = find_safe_pentagonal_face(H, E)
if result is None:
break
boundary, externals, A = result
f_vec = [coloring[e] for e in externals]
i_t = valid_index(f_vec, A)
if i_t is None:
break
v_n = next_vn
next_vn += 1
H_new = H.copy()
for v in boundary:
H_new.delete_vertex(v)
H_new.add_vertex(v_n)
side_0 = (v_n, A[i_t])
spike = (v_n, A[(i_t + 1) % 5])
side_1 = (v_n, 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])
if H_new.has_multiple_edges() or H_new.has_loops():
break
if not H_new.is_planar(set_embedding=True):
break
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)] = 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]
E |= {frozenset(spike), frozenset(side_0), frozenset(side_1),
frozenset(merged)}
return H
def main(max_n=15, time_budget_sec=600):
start = time.time()
violations = []
for n in range(12, max_n + 1):
if time.time() - start > time_budget_sec:
print(f"Time budget exhausted at n={n}.")
break
print(f"\n=== n = {n} ===")
try:
triangulations = list(graphs.triangulations(n, minimum_degree=5))
except Exception as ex:
print(f" cannot enumerate: {ex}")
continue
for tri_idx, G in enumerate(triangulations, start=1):
if time.time() - start > time_budget_sec:
break
G_copy = G.copy()
G_copy.is_planar(set_embedding=True)
D = dual_of(G_copy)
D.is_planar(set_embedding=True)
for face in D.faces():
if len(face) != 5:
continue
for i_red in range(5):
if time.time() - start > time_budget_sec:
break
res = apply_reduction(D, face, i_red, 9999)
if res is None:
continue
H_1 = res['H']
n_col_h1, kc_h1 = num_kempe_classes(H_1)
if kc_h1 == 0:
continue
# enumerate all proper colorings of H_1 to vary phi_1
edges_1, colorings_1 = proper_3_edge_colorings(H_1)
# try EVERY phi_1 (might be slow for large H_1)
max_kc_hf = 0
h_final_seen = {} # signature -> classes
for phi_1 in colorings_1:
phi_1_dict = {frozenset((e[0], e[1])): c
for e, c in zip(edges_1, phi_1)}
H_final = run_algorithm_to_completion(
H_1, phi_1_dict, res['named'])
# signature = (|V|, |E|, sorted edge tuples)
sig = (H_final.order(), H_final.size())
if sig in h_final_seen:
continue
_, kc_hf = num_kempe_classes(H_final)
h_final_seen[sig] = kc_hf
if kc_hf > max_kc_hf:
max_kc_hf = kc_hf
over = max_kc_hf > kc_h1
flag = "*** OVER ***" if over else "ok"
print(f" n={n} tri#{tri_idx} face|{len(face)} i_red={i_red}: "
f"H_1 kc={kc_h1}, max H_t* kc over {len(h_final_seen)} "
f"distinct outputs = {max_kc_hf} {flag}")
if over:
violations.append((n, tri_idx, i_red, kc_h1, max_kc_hf))
sys.stdout.flush()
break # first face only
print()
if violations:
print(f"Counterexamples (#K(H_t*) > #K(H_1)): {len(violations)}")
for v in violations:
print(f" {v}")
else:
print("No counterexamples found within tested range.")
if __name__ == '__main__':
main()
@@ -0,0 +1,464 @@
"""Lift the recoloured + bridged H_1 back to G' (dual of the n=14
triangulation), producing a proper 3-edge-colouring of the modified G'.
The modified G' = G' with: subdivisions Y_1, Y_2 of the two green witness
edges, plus a new red edge Y_1-Y_2.
Run with: sage experiments/draw_lift_to_Gprime.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import matplotlib.pyplot as plt
import math
import os
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
"""Same Tutte barycentric layout as in draw_iterated_reduction_n14.py."""
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']
DARK = '#374151'
HIGHLIGHT = '#fef3c7'
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
return Graph(
[(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
multiedges=False, loops=False)
def proper_3_edge_colorings(G):
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(tuple(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_set(edges, col_list, start_idx, color_pair):
a, b = color_pair
if col_list[start_idx] not in (a, b): return set()
in_sub = set(i for i in range(len(edges)) if col_list[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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
def trace_kempe_cycle(edges, col_list, start_idx, color_pair):
cycle_set = kempe_cycle_set(edges, col_list, start_idx, color_pair)
incident_at = {}
for ei in cycle_set:
u, v = edges[ei][0], edges[ei][1]
incident_at.setdefault(u, []).append(ei)
incident_at.setdefault(v, []).append(ei)
start_u, start_v = edges[start_idx][0], edges[start_idx][1]
walk = [(start_idx, start_v)]
cur_e = start_idx
cur_leave = start_v
while True:
nbrs = incident_at[cur_leave]
if len(nbrs) != 2: break
nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1]
u2, v2 = edges[nxt][0], edges[nxt][1]
leave_next = v2 if u2 == cur_leave else u2
if nxt == start_idx: break
walk.append((nxt, leave_next))
cur_e = nxt
cur_leave = leave_next
return walk
def matches_chord_apex_kempe(edges, col, named):
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()): 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_set(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_set(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 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 or 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])
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
return {
'H': H, 'A': A, 'boundary': boundary, 'face': face, 'i_red': i,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
def find_first_match():
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 = res['H'], res['named']
edges, gen = proper_3_edge_colorings(H)
for col in gen:
if matches_chord_apex_kempe(edges, col, named):
coloring_dict = {frozenset((e[0], e[1])): c
for e, c in zip(edges, col)}
return G, D, res, H, coloring_dict
return None
def find_conj_witness(H, edges, col_list, named):
GREEN, BLUE = 1, 2
merged_idx = edge_idx(edges, named['merged'])
kc_gb = kempe_cycle_set(edges, col_list, merged_idx, (GREEN, BLUE))
if merged_idx not in kc_gb: return None
for face in H.faces():
face_edge_ids = []
for u, v in face:
ei = edge_idx(edges, frozenset((u, v)))
if ei is not None:
face_edge_ids.append(ei)
green_on_face_in_kc = [ei for ei in face_edge_ids
if col_list[ei] == GREEN
and ei in kc_gb
and ei != merged_idx]
if len(green_on_face_in_kc) >= 2:
return face, green_on_face_in_kc[0], green_on_face_in_kc[1], kc_gb
return None
def main():
print("Setting up ...")
G14, D, red_info, H, coloring = find_first_match()
i_red = red_info['i_red']
boundary_in_D = red_info['boundary']
A_in_D = red_info['A']
named_in_D = red_info['named']
print(f" Found: i_red = {i_red}")
print(f" G' has |V|={D.order()}, |E|={D.size()}")
# Relabel H so all vertices are integers
H_relabel_map = {v: i for i, v in enumerate(H.vertex_iterator())}
inv_relabel = {i: v for v, i in H_relabel_map.items()}
H.relabel(perm=H_relabel_map, inplace=True)
coloring = {frozenset(H_relabel_map[u] for u in e): c
for e, c in coloring.items()}
named_H = {role: frozenset(H_relabel_map[u] for u in e)
for role, e in named_in_D.items()}
H.is_planar(set_embedding=True)
edges_H = list(H.edges(labels=False))
col_list = [coloring[frozenset((u, v))] for (u, v) in edges_H]
witness = find_conj_witness(H, edges_H, col_list, named_H)
face_w, e1, e2, kc_gb = witness
e1_uv = tuple(edges_H[e1]); e2_uv = tuple(edges_H[e2])
print(f" Witness in H_1 (relabeled): e1 = {e1_uv}, e2 = {e2_uv}")
e1_D = (inv_relabel[e1_uv[0]], inv_relabel[e1_uv[1]])
e2_D = (inv_relabel[e2_uv[0]], inv_relabel[e2_uv[1]])
print(f" Witness in D labels: e1 = {e1_D}, e2 = {e2_D}")
# Build modified G'
G_mod = D.copy()
max_label = max(v for v in D.vertices(sort=False) if isinstance(v, int))
Y1 = max_label + 1
Y2 = Y1 + 1
G_mod.add_vertex(Y1); G_mod.add_vertex(Y2)
G_mod.delete_edge(e1_D); G_mod.delete_edge(e2_D)
G_mod.add_edges([(e1_D[0], Y1), (Y1, e1_D[1]),
(e2_D[0], Y2), (Y2, e2_D[1]),
(Y1, Y2)])
assert G_mod.is_planar(set_embedding=True)
print(f" Modified G': |V|={G_mod.order()}, |E|={G_mod.size()}")
# Trace Kempe cycle from merged
merged_idx = edge_idx(edges_H, named_H['merged'])
walk = trace_kempe_cycle(edges_H, col_list, merged_idx, (1, 2))
walk_edges = [w[0] for w in walk]
leave_at = [w[1] for w in walk]
# Map relabeled-H vertex -> D label if not v_n_1, else None
def H_to_D(v):
d = inv_relabel[v]
return d if d != '__v_n_1__' else None
# Build new coloring on G_mod's edges.
# Step A: copy non-named, non-e1, non-e2, non-v_n_1 edges' original colors.
new_coloring = {}
named_H_set = set(named_H.values())
for e_fs, c in coloring.items():
if e_fs in named_H_set: continue
if e_fs == frozenset(e1_uv) or e_fs == frozenset(e2_uv): continue
u, v = tuple(e_fs)
du, dv = H_to_D(u), H_to_D(v)
if du is None or dv is None: continue
new_coloring[frozenset((du, dv))] = c
# Step B: walk K' (subdivided cycle) starting from merged.
# For each old cycle edge that is NOT spike/side_0/side_1/merged
# (i.e., not involving v_n_1, not the chord), we OVERWRITE its color
# via the alternation. For e1, e2 we instead emit the two subdivision
# halves. For named edges (spike, side_1, merged), they don't exist
# in G_mod, so we record the would-be color to use later for f-vector.
pos = 0
cycle_label = {} # e_fs in D labels -> new color (for cycle edges in G_mod)
half_label = {} # halves' colors
would_be = {} # named role -> would-be color after recoloring
for k, ei in enumerate(walk_edges):
leaving_vertex = leave_at[k]
u, v = edges_H[ei][0], edges_H[ei][1]
entry_vertex = v if leaving_vertex == u else u
e_fs_H = frozenset((u, v))
if ei == e1:
c0 = 2 if pos % 2 == 0 else 1; pos += 1
c1 = 2 if pos % 2 == 0 else 1; pos += 1
half_label[frozenset((inv_relabel[entry_vertex], Y1))] = c0
half_label[frozenset((Y1, inv_relabel[leaving_vertex]))] = c1
elif ei == e2:
c0 = 2 if pos % 2 == 0 else 1; pos += 1
c1 = 2 if pos % 2 == 0 else 1; pos += 1
half_label[frozenset((inv_relabel[entry_vertex], Y2))] = c0
half_label[frozenset((Y2, inv_relabel[leaving_vertex]))] = c1
else:
c = 2 if pos % 2 == 0 else 1; pos += 1
# is this a named edge (spike/side_1/merged)? Note: it can't be
# side_0 since side_0 is red and the cycle is green/blue.
for role, ef in named_H.items():
if ef == e_fs_H:
would_be[role] = c
break
else:
# Regular cycle edge: convert to D labels
du, dv = H_to_D(u), H_to_D(v)
if du is None or dv is None:
# Shouldn't happen for non-named edges
pass
else:
cycle_label[frozenset((du, dv))] = c
# Apply cycle relabeling
for e_fs, c in cycle_label.items():
new_coloring[e_fs] = c
# Apply half colors
for e_fs, c in half_label.items():
new_coloring[e_fs] = c
# New red edge
new_coloring[frozenset((Y1, Y2))] = 0
print(f" Would-be colors of named H_1 edges after recolor: {would_be}")
# would_be may not include side_0 (not on cycle); side_0's color is
# whatever it was in original (= red = 0).
if 'side_0' not in would_be:
would_be['side_0'] = coloring[named_H['side_0']] # 0 (red)
# Step C: color the 5 externals f_k = B_k - A_k.
# At each A_k, the third color = would_be color of the missing H_1 edge.
def third(c1, c2): return ({0, 1, 2} - {c1, c2}).pop()
externals_colors = [None] * 5
for k, B_k in enumerate(boundary_in_D):
A_k = A_in_D[k]
# Which named role was at A_k in H_1?
# A_in_D[i_red] = side_0's endpoint, A_in_D[i_red+1] = spike's,
# A_in_D[i_red+2] = side_1's, A_in_D[i_red+3,+4] = merged.
if k == i_red: role = 'side_0'
elif k == (i_red+1) % 5: role = 'spike'
elif k == (i_red+2) % 5: role = 'side_1'
else: role = 'merged'
c_ext = would_be[role]
new_coloring[frozenset((B_k, A_k))] = c_ext
externals_colors[k] = c_ext
print(f" f-vector at F_v: {externals_colors}")
# Step D: 5 boundary edges B_k - B_{k+1} via Lemma 2.4.
boundary_colors = [None] * 5
for k in range(5):
f_k = externals_colors[k]
f_kp1 = externals_colors[(k + 1) % 5]
if f_k != f_kp1:
boundary_colors[k] = third(f_k, f_kp1)
for _ in range(20):
changed = False
for k in range(5):
if boundary_colors[k] is not None: continue
f_k = externals_colors[k]
prev_c = boundary_colors[(k - 1) % 5]
next_c = boundary_colors[(k + 1) % 5]
forbidden = {f_k}
if prev_c is not None: forbidden.add(prev_c)
if next_c is not None: forbidden.add(next_c)
allowed = list({0, 1, 2} - forbidden)
if allowed:
boundary_colors[k] = allowed[0]
changed = True
if not changed: break
for k in range(5):
if boundary_colors[k] is None:
print(f" WARNING: undetermined boundary color at k={k}")
return
B_k = boundary_in_D[k]; B_kp1 = boundary_in_D[(k + 1) % 5]
new_coloring[frozenset((B_k, B_kp1))] = boundary_colors[k]
# Sanity check
bad = []
for v in G_mod.vertices(sort=False):
seen = []
for w in G_mod.neighbor_iterator(v):
seen.append(new_coloring.get(frozenset((v, w))))
if None in seen or len(set(seen)) != len(seen):
bad.append((v, seen))
if bad:
print(f" PROPRIETY FAILS at vertices:")
for v, s in bad[:5]:
print(f" {v}: {s}")
else:
print(" Propriety holds at every vertex of modified G'.")
# Use H_1's Tutte layout for vertices shared between H_1 and G' (the 19
# surviving vertices). Then add the 5 boundary vertices B_k of partial F_v
# by running a local barycenter iteration with the shared vertices fixed.
H.is_planar(set_embedding=True)
H_pos = tutte_layout(H,
avoid_verts={H_relabel_map['__v_n_1__']})
# Map H_1 positions back to D labels (skip v_n_1)
pos_layout = {}
for v_H, p in H_pos.items():
v_D = inv_relabel[v_H]
if v_D != '__v_n_1__':
pos_layout[v_D] = p
# Place B_k's by iterating barycenter of their 3 neighbours in G_mod
# (B_{k-1}, B_{k+1}, A_k); start each B_k near its A_k.
B_pos = {B_k: pos_layout[A_in_D[k]]
for k, B_k in enumerate(boundary_in_D)}
for _ in range(300):
new_B = {}
for k, B_k in enumerate(boundary_in_D):
B_prev = boundary_in_D[(k - 1) % 5]
B_next = boundary_in_D[(k + 1) % 5]
A_k = A_in_D[k]
sx = (B_pos[B_prev][0] + B_pos[B_next][0] + pos_layout[A_k][0]) / 3
sy = (B_pos[B_prev][1] + B_pos[B_next][1] + pos_layout[A_k][1]) / 3
new_B[B_k] = (sx, sy)
B_pos = new_B
for B_k, p in B_pos.items():
pos_layout[B_k] = p
# Y_1, Y_2 at midpoints of their subdivided edges e1, e2
pos_layout[Y1] = ((pos_layout[e1_D[0]][0] + pos_layout[e1_D[1]][0]) / 2,
(pos_layout[e1_D[0]][1] + pos_layout[e1_D[1]][1]) / 2)
pos_layout[Y2] = ((pos_layout[e2_D[0]][0] + pos_layout[e2_D[1]][0]) / 2,
(pos_layout[e2_D[0]][1] + pos_layout[e2_D[1]][1]) / 2)
fig, ax = plt.subplots(figsize=(8, 8))
for u, v, _ in G_mod.edges():
e = frozenset((u, v))
c = C[new_coloring[e]]
lw = 3.4 if e == frozenset((Y1, Y2)) else 1.5
(x0, y0), (x1, y1) = pos_layout[u], pos_layout[v]
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
for v in G_mod.vertices(sort=False):
x, y = pos_layout[v]
if v in (Y1, Y2):
ax.scatter(x, y, s=140, color=DARK, edgecolors='white',
linewidths=1.6, zorder=6)
else:
ax.scatter(x, y, s=55, color=DARK, zorder=3)
ax.set_aspect('equal')
ax.axis('off')
out_path = os.path.join(OUT_DIR, 'fig_lift_to_Gprime.png')
fig.savefig(out_path, dpi=170, bbox_inches='tight')
plt.close(fig)
print(f"Wrote {out_path}")
if __name__ == '__main__':
main()
@@ -0,0 +1,277 @@
"""Draw a Conjecture 3.6 witness: on H_1 with its chord-apex+Kempe coloring,
find a face with two green edges that lie (with the merged edge) on a common
{green, blue}-Kempe cycle. Subdivide both green edges with new vertices and
join the two new vertices by a new red edge.
Run with: sage experiments/draw_step1_conj36.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
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
C = ['#dc2626', '#16a34a', '#2563eb'] # 0=red 1=green 2=blue
GRAY = '#9ca3af'
DARK = '#374151'
HIGHLIGHT = '#fef3c7'
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
return Graph(
[(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
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 or 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])
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
return {
'H': H, 'A': A,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
def proper_3_edge_colorings(G):
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(tuple(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, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b): return set()
in_sub = set(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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
def matches_chord_apex_kempe(edges, col, named):
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()): 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():
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 = res['H'], res['named']
edges, gen = proper_3_edge_colorings(H)
for col in gen:
if matches_chord_apex_kempe(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, coloring_dict
return None
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
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
def find_conj_witness(H, edges, col_list, named):
"""Find a face F of H with two distinct green edges e1, e2, NEITHER equal
to the merged edge, such that e1, e2, merged all lie on the
{green, blue}-Kempe cycle through merged."""
GREEN, BLUE = 1, 2
merged_idx = edge_idx(edges, named['merged'])
kc_gb = kempe_cycle(edges, col_list, merged_idx, (GREEN, BLUE))
if merged_idx not in kc_gb:
return None
for face in H.faces():
face_edge_ids = []
for u, v in face:
ei = edge_idx(edges, frozenset((u, v)))
if ei is not None:
face_edge_ids.append(ei)
green_on_face_in_kc = [ei for ei in face_edge_ids
if col_list[ei] == GREEN
and ei in kc_gb
and ei != merged_idx]
if len(green_on_face_in_kc) >= 2:
return face, green_on_face_in_kc[0], green_on_face_in_kc[1], kc_gb
return None
def main():
print("Searching for the first n=14 chord-apex+Kempe match ...")
result = find_first_match()
G14, D, face_chosen, i_red, H, named, coloring = result
print(f" Found: i_red = {i_red}")
H_relabel_map = {v: i for i, v in enumerate(H.vertex_iterator())}
H.relabel(perm=H_relabel_map, inplace=True)
vn = H_relabel_map['__v_n_1__']
coloring = {frozenset(H_relabel_map[u] for u in e): c
for e, c in coloring.items()}
named = {role: frozenset(H_relabel_map[u] for u in e)
for role, e in named.items()}
H.is_planar(set_embedding=True)
pos = tutte_layout(H, avoid_verts={vn})
E_protected = set(named.values())
# Build (edges, coloring) in list/tuple form to use kempe helpers
edges = list(H.edges(labels=False))
col_list = [coloring[frozenset((u, v))] for (u, v) in edges]
witness = find_conj_witness(H, edges, col_list, named)
if witness is None:
print("ERROR: no witness found.")
return
face_w, e1, e2, kc_gb = witness
e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2])
print(f" Witness face has {len(face_w)} edges.")
print(f" e1 = {e1_uv}, e2 = {e2_uv}")
print(f" {{green, blue}}-Kempe cycle through merged: {len(kc_gb)} edges.")
# Midpoints in the layout
mp1 = ((pos[e1_uv[0]][0] + pos[e1_uv[1]][0]) / 2,
(pos[e1_uv[0]][1] + pos[e1_uv[1]][1]) / 2)
mp2 = ((pos[e2_uv[0]][0] + pos[e2_uv[1]][0]) / 2,
(pos[e2_uv[0]][1] + pos[e2_uv[1]][1]) / 2)
# Draw
fig, ax = plt.subplots(figsize=(8, 8))
for u, v, _ in H.edges():
e = frozenset([u, v])
c = C[coloring[e]]
lw = 3.8 if e in E_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 H.vertices(sort=False):
x, y = pos[v]
if v == vn:
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
edgecolors='black', linewidths=1.2, zorder=4)
ax.annotate('$v_n^{(1)}$', (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)
# New red edge between midpoints
ax.plot([mp1[0], mp2[0]], [mp1[1], mp2[1]],
color=C[0], lw=4.0, zorder=5)
# New vertices
for (mx, my) in (mp1, mp2):
ax.scatter(mx, my, s=130, color=DARK, edgecolors='white',
linewidths=1.6, zorder=6)
ax.set_aspect('equal')
ax.axis('off')
out_path = os.path.join(OUT_DIR, 'fig_alg_step1_conj36.png')
fig.savefig(out_path, dpi=170, bbox_inches='tight')
plt.close(fig)
print(f"Wrote {out_path}")
if __name__ == '__main__':
main()
@@ -0,0 +1,373 @@
"""Build the Conjecture 3.6 witness on H_1, subdivide the two green edges
with new vertices X1, X2, add a new red edge X1-X2, then recolor the
{green, blue}-Kempe cycle through merged so that walking it from merged
gives the alternating sequence blue, green, blue, green, ... (equivalently,
a Kempe swap of green/blue along the cycle).
Edges off the cycle keep their original colour; the new red edge stays red.
Propriety holds throughout: at every old vertex the 2 cycle edges still
alternate green/blue and the off-cycle edge is red; at X1 and X2 the two
cycle halves are forced to opposite colours of {green, blue} and the new
red edge supplies the third colour.
Run with: sage experiments/draw_step1_conj36_recolored.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
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
C = ['#dc2626', '#16a34a', '#2563eb'] # 0=red 1=green 2=blue
GRAY = '#9ca3af'
DARK = '#374151'
HIGHLIGHT = '#fef3c7'
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
return Graph(
[(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
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 or 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])
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
return {
'H': H, 'A': A,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
def proper_3_edge_colorings(G):
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(tuple(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, col_list, start_idx, color_pair):
a, b = color_pair
if col_list[start_idx] not in (a, b): return set()
in_sub = set(i for i in range(len(edges)) if col_list[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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
def trace_kempe_cycle(edges, col_list, start_idx, color_pair):
"""Walk the (a,b)-Kempe cycle through edges[start_idx]. Returns a list
of (edge_idx, direction) where direction is the vertex you LEAVE the
edge by. The returned list is in cyclic order, length 2L."""
a, b = color_pair
cycle_edges = kempe_cycle_edges(edges, col_list, start_idx, color_pair)
# build adjacency: at each vertex on the cycle, the 2 (a,b)-edges incident
incident_at = {}
for ei in cycle_edges:
u, v = edges[ei][0], edges[ei][1]
incident_at.setdefault(u, []).append(ei)
incident_at.setdefault(v, []).append(ei)
# walk
start_u, start_v = edges[start_idx][0], edges[start_idx][1]
walk = [(start_idx, start_v)]
cur_e = start_idx
cur_leave = start_v
while True:
# at cur_leave, the 2 cycle edges are cur_e and the next one
nbrs = incident_at[cur_leave]
if len(nbrs) != 2: break
nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1]
u2, v2 = edges[nxt][0], edges[nxt][1]
leave_next = v2 if u2 == cur_leave else u2
if nxt == start_idx: break
walk.append((nxt, leave_next))
cur_e = nxt
cur_leave = leave_next
return walk
def matches_chord_apex_kempe(edges, col, named):
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()): 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(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(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():
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 = res['H'], res['named']
edges, gen = proper_3_edge_colorings(H)
for col in gen:
if matches_chord_apex_kempe(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, coloring_dict
return None
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
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
def find_conj_witness(H, edges, col_list, named):
GREEN, BLUE = 1, 2
merged_idx = edge_idx(edges, named['merged'])
kc_gb = kempe_cycle_edges(edges, col_list, merged_idx, (GREEN, BLUE))
if merged_idx not in kc_gb:
return None
for face in H.faces():
face_edge_ids = []
for u, v in face:
ei = edge_idx(edges, frozenset((u, v)))
if ei is not None:
face_edge_ids.append(ei)
green_on_face_in_kc = [ei for ei in face_edge_ids
if col_list[ei] == GREEN
and ei in kc_gb
and ei != merged_idx]
if len(green_on_face_in_kc) >= 2:
return face, green_on_face_in_kc[0], green_on_face_in_kc[1], kc_gb
return None
def main():
print("Setting up ...")
result = find_first_match()
G14, D, face_chosen, i_red, H, named, coloring = result
print(f" Found: i_red = {i_red}")
H_relabel_map = {v: i for i, v in enumerate(H.vertex_iterator())}
H.relabel(perm=H_relabel_map, inplace=True)
vn = H_relabel_map['__v_n_1__']
coloring = {frozenset(H_relabel_map[u] for u in e): c
for e, c in coloring.items()}
named = {role: frozenset(H_relabel_map[u] for u in e)
for role, e in named.items()}
H.is_planar(set_embedding=True)
pos = tutte_layout(H, avoid_verts={vn})
edges = list(H.edges(labels=False))
col_list = [coloring[frozenset((u, v))] for (u, v) in edges]
witness = find_conj_witness(H, edges, col_list, named)
face_w, e1, e2, kc_gb = witness
e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2])
merged_idx = edge_idx(edges, named['merged'])
print(f" Witness: e1 = {e1_uv}, e2 = {e2_uv}, |kc_gb| = {len(kc_gb)}")
# Trace the Kempe cycle starting from merged
walk = trace_kempe_cycle(edges, col_list, merged_idx, (1, 2))
walk_edges_in_order = [w[0] for w in walk]
leave_in_order = [w[1] for w in walk]
print(f" Kempe cycle (in order from merged): length {len(walk_edges_in_order)}")
# Compute midpoints
mp1 = ((pos[e1_uv[0]][0] + pos[e1_uv[1]][0]) / 2,
(pos[e1_uv[0]][1] + pos[e1_uv[1]][1]) / 2)
mp2 = ((pos[e2_uv[0]][0] + pos[e2_uv[1]][0]) / 2,
(pos[e2_uv[0]][1] + pos[e2_uv[1]][1]) / 2)
# Construct the new graph: H with e1, e2 subdivided and new red edge X1-X2.
H_new = H.copy()
X1 = max(H.vertices(sort=False)) + 1
X2 = X1 + 1
H_new.add_vertex(X1); H_new.add_vertex(X2)
H_new.delete_edge(e1_uv)
H_new.delete_edge(e2_uv)
e1_a, e1_b = e1_uv
e2_a, e2_b = e2_uv
H_new.add_edges([(e1_a, X1), (X1, e1_b),
(e2_a, X2), (X2, e2_b),
(X1, X2)])
pos_new = dict(pos); pos_new[X1] = mp1; pos_new[X2] = mp2
# Build subdivided cycle K' as a list of (edge_frozenset, "entry vertex
# in original direction") in cyclic order starting from merged.
K_prime = []
for k, ei in enumerate(walk_edges_in_order):
leaving_vertex = leave_in_order[k]
u, v = edges[ei][0], edges[ei][1]
entry_vertex = v if leaving_vertex == u else u
if ei == e1:
K_prime.append(frozenset((entry_vertex, X1)))
K_prime.append(frozenset((X1, leaving_vertex)))
elif ei == e2:
K_prime.append(frozenset((entry_vertex, X2)))
K_prime.append(frozenset((X2, leaving_vertex)))
else:
K_prime.append(frozenset((u, v)))
print(f" Subdivided cycle length: {len(K_prime)}")
# Recolor K' alternately starting from merged = blue
new_coloring = dict(coloring)
new_coloring.pop(frozenset(e1_uv), None)
new_coloring.pop(frozenset(e2_uv), None)
for k, e_fs in enumerate(K_prime):
new_coloring[e_fs] = 2 if k % 2 == 0 else 1 # blue, green, blue, green
# New red edge X1-X2
new_coloring[frozenset((X1, X2))] = 0
# Sanity: check propriety at every vertex of H_new
bad = False
for v in H_new.vertices(sort=False):
seen = []
for w in H_new.neighbor_iterator(v):
seen.append(new_coloring[frozenset((v, w))])
if len(set(seen)) != len(seen):
print(f" *** propriety FAILS at vertex {v}: colors {seen}")
bad = True
if not bad:
print(" Propriety holds at every vertex.")
# Identify which edges of H_new are "protected" (the original named edges,
# minus e1, e2 which have been subdivided away).
E_protected = set()
for role, ef in named.items():
if ef in (frozenset(e1_uv), frozenset(e2_uv)):
continue
E_protected.add(ef)
# Draw
fig, ax = plt.subplots(figsize=(8, 8))
for u, v, _ in H_new.edges():
e = frozenset([u, v])
c = C[new_coloring[e]]
if e == frozenset((X1, X2)):
lw = 4.0
elif e in E_protected:
lw = 3.8
else:
lw = 1.4
(x0, y0), (x1, y1) = pos_new[u], pos_new[v]
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
for v in H_new.vertices(sort=False):
x, y = pos_new[v]
if v == vn:
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
edgecolors='black', linewidths=1.2, zorder=4)
ax.annotate('$v_n^{(1)}$', (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))
elif v in (X1, X2):
ax.scatter(x, y, s=130, color=DARK, edgecolors='white',
linewidths=1.6, zorder=6)
else:
ax.scatter(x, y, s=70, color=DARK, zorder=3)
ax.set_aspect('equal')
ax.axis('off')
out_path = os.path.join(OUT_DIR, 'fig_alg_step1_conj36_recolored.png')
fig.savefig(out_path, dpi=170, bbox_inches='tight')
plt.close(fig)
print(f"Wrote {out_path}")
if __name__ == '__main__':
main()
@@ -0,0 +1,380 @@
"""Search for a counterexample to Conjecture 3.6.
For each min-deg-5 triangulation G and each reduced dual H_1, find a proper
3-edge-coloring phi_1 of H_1 that satisfies (i) chord-apex (spike = merged)
and (ii) both Kempe-chain conditions of Lemma 2.7 -- i.e., the kind of
coloring that a minimal counterexample's reduced dual would be forced to
have. Run Algorithm 3.1 to completion from phi_1, get the final colouring
phi_t* on H_t*. Then enumerate all proper 3-edge-colorings of H_t*, find the
Kempe class containing phi_t*, and check whether any coloring in that class
is "all-distinct" (every spike_t != merged_t for t = 1, ..., t*).
If no such all-distinct coloring exists in phi_t*'s Kempe class:
counterexample to Conjecture 3.6 found.
Run with: sage experiments/search_conj_3_6_counterexample.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
import time
def dual_of(G):
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
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])
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
return {
'H': H, 'A': A, 'boundary': boundary,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
def proper_3_edge_colorings(G):
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(tuple(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 edge_idx(edges, e_frozen):
for i, e in enumerate(edges):
if frozenset((e[0], e[1])) == e_frozen:
return i
return None
def kempe_cycle(edges, coloring, start_idx, color_pair):
a, b = color_pair
if coloring[start_idx] not in (a, b):
return set()
in_sub = set(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_chord_apex_kempe(edges, col, named):
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
if any(v is None for v in idx.values()):
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_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 = []
ok = True
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
ok = False
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
if not ok:
continue
if any(e in protected for e in boundary_edges + externals):
continue
return boundary, externals, A
return None
def valid_index(f_vec, A):
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
return i
return None
def run_algorithm_to_completion(H, coloring_dict, named_step1, vn_start=1000):
"""Returns (H_final, coloring_final, list_of_named_dicts)."""
H = H.copy()
coloring = dict(coloring_dict)
all_named = [named_step1]
E = set(named_step1.values())
next_vn = vn_start
while True:
H.is_planar(set_embedding=True)
result = find_safe_pentagonal_face(H, E)
if result is None:
break
boundary, externals, A = result
f_vec = [coloring[e] for e in externals]
i_t = valid_index(f_vec, A)
if i_t is None:
break
v_n = next_vn
next_vn += 1
H_new = H.copy()
for v in boundary:
H_new.delete_vertex(v)
H_new.add_vertex(v_n)
side_0 = (v_n, A[i_t])
spike = (v_n, A[(i_t + 1) % 5])
side_1 = (v_n, 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])
if H_new.has_multiple_edges() or H_new.has_loops():
break
if not H_new.is_planar(set_embedding=True):
break
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)] = 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())
all_named.append(named)
return H, coloring, all_named
def kempe_class_via_union_find(edges, colorings):
"""For each coloring, attempt every Kempe swap and union. Returns parent
array of disjoint-set forest."""
n = len(colorings)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
col_idx = {c: i for i, c in enumerate(colorings)}
for c_idx, col in enumerate(colorings):
for pair in [(0, 1), (0, 2), (1, 2)]:
visited = set()
for start in range(len(edges)):
if col[start] not in pair or start in visited:
continue
cyc = kempe_cycle(edges, col, start, pair)
visited |= cyc
a, b = pair
new_col = list(col)
for k in cyc:
if new_col[k] == a:
new_col[k] = b
elif new_col[k] == b:
new_col[k] = a
new_col = tuple(new_col)
if new_col != col and new_col in col_idx:
union(c_idx, col_idx[new_col])
return [find(i) for i in range(n)]
def is_all_distinct(edges, col, all_named):
for named in all_named:
i_spike = edge_idx(edges, named['spike'])
i_merged = edge_idx(edges, named['merged'])
if i_spike is None or i_merged is None:
return False
if col[i_spike] == col[i_merged]:
return False
return True
def check_one_triangulation(G, n, idx_label, time_budget):
G_planar = G.is_planar(set_embedding=True)
if not G_planar:
return None
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, 999)
if res is None:
continue
H_1, named_1 = res['H'], res['named']
edges_1, colorings_1 = proper_3_edge_colorings(H_1)
candidates = [col for col in colorings_1
if matches_chord_apex_kempe(edges_1, col, named_1)]
if not candidates:
continue
# For the first candidate, run algorithm and check.
phi_1 = candidates[0]
coloring_dict = {frozenset((e[0], e[1])): c
for e, c in zip(edges_1, phi_1)}
H_final, phi_final_dict, all_named = run_algorithm_to_completion(
H_1, coloring_dict, named_1)
edges_final, colorings_final = proper_3_edge_colorings(H_final)
# find phi_final as a tuple
phi_final = tuple(
phi_final_dict[frozenset((e[0], e[1]))] for e in edges_final
)
if phi_final not in colorings_final:
# not in enumeration, skip
continue
print(f" n={n}, tri#{idx_label}, face_size={len(face)}, "
f"i_red={i_red}: H_1 has {len(candidates)} chord+Kempe "
f"colorings; algorithm produced H_t* with |V|={H_final.order()}, "
f"|E|={H_final.size()}; H_t* has {len(colorings_final)} "
f"proper colorings.")
# Kempe classes
parent = kempe_class_via_union_find(edges_final, colorings_final)
phi_final_idx = colorings_final.index(phi_final)
phi_class = parent[phi_final_idx]
n_in_class = sum(1 for p in parent if p == phi_class)
print(f" phi_final's Kempe class has {n_in_class} colorings; "
f"total classes: {len(set(parent))}")
# search for all-distinct in same class
for ci, p in enumerate(parent):
if p != phi_class:
continue
if is_all_distinct(edges_final, colorings_final[ci], all_named):
print(f" found all-distinct coloring in same Kempe "
f"class -- Conj 3.6 holds for this instance.")
break
else:
print(f" *** NO all-distinct coloring in phi_final's "
f"Kempe class -- CONJECTURE 3.6 FAILS HERE ***")
return {
'n': n, 'tri': idx_label, 'face_size': len(face),
'i_red': i_red, 'H_final': H_final,
'phi_final': phi_final, 'all_named': all_named,
}
return None
def main(max_n=16, time_budget_sec=600):
start = time.time()
for n in range(12, max_n + 1):
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) ===")
try:
it = graphs.triangulations(n, minimum_degree=5)
except Exception as ex:
print(f" cannot enumerate: {ex}")
continue
for idx, G in enumerate(it, start=1):
if time.time() - start > time_budget_sec:
print(f"Time budget exhausted mid-n={n}.")
return
hit = check_one_triangulation(G, n, idx,
time_budget_sec - (time.time() - start))
if hit is not None:
print()
print(f"=== Counterexample to Conjecture 3.6 ===")
print(f" n = {hit['n']}, triangulation #{hit['tri']}, "
f"i_red = {hit['i_red']}")
print(f" H_t* has |V|={hit['H_final'].order()}, "
f"|E|={hit['H_final'].size()}")
return
sys.stdout.flush()
if __name__ == '__main__':
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

@@ -17,12 +17,14 @@
\newlabel{alg:iterated-reduction}{{3.1}{7}}
\newlabel{rem:alg-invariants}{{3.2}{7}}
\newlabel{rem:alg-chord-apex}{{3.3}{7}}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Algorithm\nonbreakingspace 3.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 condition (Lemma\nonbreakingspace 2.6\hbox {}) and the Kempe-cycle condition (Lemma\nonbreakingspace 2.7\hbox {}), 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.}}{8}{}\protected@file@percent }
\newlabel{fig:iterated-reduction-trace}{{3}{8}}
\newlabel{lem:exactly-one-match}{{3.4}{8}}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{0pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Algorithm\nonbreakingspace 3.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 condition (Lemma\nonbreakingspace 2.6\hbox {}) and the Kempe-cycle condition (Lemma\nonbreakingspace 2.7\hbox {}), 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.}}{8}{}\protected@file@percent }
\newlabel{fig:iterated-reduction-trace}{{3}{8}}
\newlabel{conj:no-all-distinct-coloring}{{3.4}{8}}
\gdef \@abspage@last{8}
\newlabel{lem:all-distinct-exists}{{3.5}{9}}
\newlabel{conj:face-monochromatic-pair-on-merged-kempe-cycle}{{3.6}{9}}
\gdef \@abspage@last{9}
@@ -1,6 +1,6 @@
# Fdb version 3
["pdflatex"] 1779490854 "/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" "paper.pdf" "paper" 1779490855
"/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" 1779490854 8400 319c3b8b7a4962c7285398f53dc0c37a ""
["pdflatex"] 1779636334 "/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" "paper.pdf" "paper" 1779636335
"/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" 1779636334 29173 3355ad986dd2045d7c83ee9e1f4197ba ""
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 ""
@@ -19,17 +19,21 @@
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1136768653 1116 933a60c408fc0a863a92debe84b2d294 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti7.tfm" 1136768653 1492 86331993fe614793f5e7e755835c31c5 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm" 1136768653 768 1321e9409b4137d6fb428ac9dc956269 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb" 1248133631 30251 6afa5cb1d0204815a708a080681d4674 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1248133631 36299 5f9df58c2139e7edcf37c8fca4bd384d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb" 1248133631 37912 77d683123f92148345f3fc36a38d9ab1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb" 1248133631 36281 c355509802a035cadc5f15869451dcee ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.pfb" 1248133631 31809 8670ca339bf94e56da1fc21c80635e2a ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1248133631 32762 224316ccc9ad3ca0423a14971cfa7fc1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1248133631 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1248133631 32569 5e5ddc8df908dea60932f3c484a54c0d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb" 1248133631 32915 7bf7720c61a5b3a7ff25b0964421c9b6 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 ""
@@ -59,12 +63,18 @@
"/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 ""
"/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc ""
"/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c ""
"fig_alg_step0.png" 1779555533 79063 a44949fedf2f79b9d1af80dda627bad0 ""
"fig_alg_step1.png" 1779555533 81138 4655a02d13b723226d9e88c98f25fc14 ""
"fig_alg_step2.png" 1779555533 78532 6a2ff0be6a6e40110e6632afdb232782 ""
"fig_chord_apex_step1.png" 1779497048 128193 2156b95264c4e7be78a47a8f38e36347 ""
"fig_chord_apex_step2.png" 1779497048 103549 502d6b9497ab67090f34da927b068fdc ""
"fig_chord_apex_step3.png" 1779497048 104380 d3d0e553ec0e4ca7ef3f4de590f42ccb ""
"fig_reduced_dual_step1.png" 1779490218 117795 4da7754ac28df9e809cfa1069e081c53 ""
"fig_reduced_dual_step2.png" 1779490218 96839 4f94c996220a2758dd0ff21ebdb9b2be ""
"fig_reduced_dual_step3.png" 1779490218 102877 d2a5db5532697441e3150e2ad26b9173 ""
"fig_reduced_dual_step4.png" 1779490218 107439 b30c7a68e32660f9bd3a8070bdb96944 ""
"paper.aux" 1779490855 1250 5e537a38e8b733147def35d69e964063 "pdflatex"
"paper.tex" 1779490854 8400 319c3b8b7a4962c7285398f53dc0c37a ""
"paper.aux" 1779636335 3620 34f7ba6436aaccd7664676641a126dbe "pdflatex"
"paper.tex" 1779636334 29173 3355ad986dd2045d7c83ee9e1f4197ba ""
(generated)
"paper.aux"
"paper.log"
@@ -247,17 +247,52 @@ INPUT ./fig_reduced_dual_step4.png
INPUT fig_reduced_dual_step4.png
INPUT ./fig_reduced_dual_step4.png
INPUT ./fig_reduced_dual_step4.png
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti7.tfm
INPUT ./fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step1.png
INPUT fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step2.png
INPUT fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step3.png
INPUT ./fig_chord_apex_step3.png
INPUT fig_chord_apex_step3.png
INPUT ./fig_chord_apex_step3.png
INPUT ./fig_chord_apex_step3.png
INPUT ./fig_alg_step0.png
INPUT ./fig_alg_step0.png
INPUT fig_alg_step0.png
INPUT ./fig_alg_step0.png
INPUT ./fig_alg_step0.png
INPUT ./fig_alg_step1.png
INPUT ./fig_alg_step1.png
INPUT fig_alg_step1.png
INPUT ./fig_alg_step1.png
INPUT ./fig_alg_step1.png
INPUT ./fig_alg_step2.png
INPUT ./fig_alg_step2.png
INPUT fig_alg_step2.png
INPUT ./fig_alg_step2.png
INPUT ./fig_alg_step2.png
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm
INPUT paper.aux
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb
@@ -1,12 +1,12 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 23 MAY 2026 13:19
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 24 MAY 2026 11:25
entering extended mode
restricted \write18 enabled.
file:line:error style messages enabled.
%&-line parsing enabled.
**paper.tex
(./paper.tex
**/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex
(/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/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
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
@@ -18,17 +18,14 @@ Package: amsmath 2021/10/15 v2.17l AMS math features
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
(/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
)) (/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
) (/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names
)
\inf@bad=\count185
@@ -69,13 +66,10 @@ LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
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
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
) (/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
@@ -106,42 +100,33 @@ LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
\thm@postskip=\skip55
\thm@headsep=\skip56
\dth@everypar=\toks26
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
) (/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
) (/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
(/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
) (/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
(/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
) (/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
(/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
(/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)
) (./paper.aux)
\openout1 = `paper.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 27.
@@ -159,17 +144,13 @@ 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
(/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
) (/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
@@ -184,18 +165,12 @@ File: umsb.fd 2013/01/14 v3.01 AMS symbols B
\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}]
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 485.
(/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 Live
)) [1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
Overfull \hbox (41.917pt too wide) in paragraph at lines 145--147
[]\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 )$.
[]\OT1/cmr/m/n/10 List the five degree-$2$ ver-tices in clock-wise or-der around $\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 )$.
[]
<fig_reduced_dual_step1.png, id=17, 517.79329pt x 499.08812pt>
@@ -221,8 +196,7 @@ Package pdftex.def Info: fig_reduced_dual_step4.png used on input line 168.
LaTeX Warning: `h' float specifier changed to `ht'.
[2] [3 <./fig_reduced_dual_step1.png> <./fig_reduced_dual_step2.png> <./fig_red
uced_dual_step3.png> <./fig_reduced_dual_step4.png>]
[2] [3 <./fig_reduced_dual_step1.png> <./fig_reduced_dual_step2.png> <./fig_reduced_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>
@@ -242,15 +216,9 @@ Package pdftex.def Info: fig_chord_apex_step3.png used on input line 291.
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]
[4] [5 <./fig_chord_apex_step1.png> <./fig_chord_apex_step2.png> <./fig_chord_apex_step3.png>] [6]
Overfull \hbox (4.76643pt too wide) in paragraph at lines 433--440
\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 )$
\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[]\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 )$
[]
<fig_alg_step0.png, id=49, 399.6106pt x 459.55217pt>
@@ -270,21 +238,17 @@ Package pdftex.def Info: fig_alg_step2.png used on input line 481.
(pdftex.def) Requested size: 115.20264pt x 132.48134pt.
Underfull \hbox (badness 4391) in paragraph at lines 498--498
\OT1/cmr/m/sc/10 Figure 3.\OT1/cmr/m/n/10 Algorithm 3.1[] on $\OML/cmm/m/it/10
G[] \OT1/cmr/m/n/10 = [](\OML/cmm/m/it/10 G\OT1/cmr/m/n/10 )$, where $\OML/cmm/
m/it/10 G$
\OT1/cmr/m/sc/10 Figure 3.\OT1/cmr/m/n/10 Algorithm 3.1[] on $\OML/cmm/m/it/10 G[] \OT1/cmr/m/n/10 = [](\OML/cmm/m/it/10 G\OT1/cmr/m/n/10 )$, where $\OML/cmm/m/it/10 G$
[]
Underfull \hbox (badness 3623) in paragraph at lines 498--498
\OT1/cmr/m/n/10 is the first min-degree-$5$ plantri tri-an-gu-la-tion on $14$ v
er-
\OT1/cmr/m/n/10 is the first min-degree-$5$ plantri tri-an-gu-la-tion on $14$ ver-
[]
Underfull \hbox (badness 3179) in paragraph at lines 498--498
\OT1/cmr/m/n/10 tices and $\OML/cmm/m/it/10 '[]$ \OT1/cmr/m/n/10 is a spe-cific
proper $3$-edge-colouring of $\OML/cmm/m/it/10 H[]$
\OT1/cmr/m/n/10 tices and $\OML/cmm/m/it/10 '[]$ \OT1/cmr/m/n/10 is a spe-cific proper $3$-edge-colouring of $\OML/cmm/m/it/10 H[]$
[]
@@ -297,37 +261,20 @@ Underfull \hbox (badness 6094) in paragraph at lines 498--498
\OT1/cmr/m/n/10 and the Kempe-cycle con-di-tion (Lemma 2.7[]), found by
[]
[7] [8 <./fig_alg_step0.png> <./fig_alg_step1.png> <./fig_alg_step2.png>]
(./paper.aux) )
[7] [8 <./fig_alg_step0.png> <./fig_alg_step1.png> <./fig_alg_step2.png>] [9] (./paper.aux) )
Here is how much of TeX's memory you used:
3079 strings out of 478268
43848 string characters out of 5846347
344259 words of memory out of 5000000
21116 multiletter control sequences out of 15000+600000
3082 strings out of 478268
44173 string characters out of 5846347
344279 words of memory out of 5000000
21118 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,1306b,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/cmsy5.pfb></usr/
local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr/lo
cal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/loc
al/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local
/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/local/
texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (8 pages, 969703 bytes).
69i,8n,76p,1392b,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/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/type1/public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/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/cmr5.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></usr/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/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (9 pages, 977618 bytes).
PDF statistics:
137 PDF objects out of 1000 (max. 8388607)
72 compressed objects within 1 object stream
140 PDF objects out of 1000 (max. 8388607)
74 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)
@@ -499,16 +499,121 @@ v_n^{(2)}$ in the interior.}
\label{fig:iterated-reduction-trace}
\end{figure}
\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'$. Lemma~\ref{lem:chord-apex} 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 Lemma~\ref{lem:pentagonal-externals}, $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
Lemma~\ref{lem:pentagonal-externals} 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 (Lemma~\ref{lem:pentagonal-externals}), 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{conjecture}
\label{conj:no-all-distinct-coloring}
Let $G$ be a maximal planar graph of minimum degree $\geq 5$, and let
$H_{t^*}$ be the final reduced graph produced by some terminating execution
of Algorithm~\ref{alg:iterated-reduction} on $G$, with the corresponding
sequence of named pairs $(\mathrm{spike}_t, \mathrm{merged}_t)$ for
$t = 1, \dots, t^*$. Then $G$ is a minimal counterexample to the Four Colour
Theorem if and only if there is no proper $3$-edge-colouring of $H_{t^*}$
under which $\mathrm{spike}_t$ and $\mathrm{merged}_t$ receive different
colours for every $t \in \{1, \dots, t^*\}$.
\label{conj:face-monochromatic-pair-on-merged-kempe-cycle}
Let $G$ be a minimal counterexample to the Four Colour Theorem, and let
$\widehat{G}'_{v,i}$ be a reduced dual of $G' = \mathrm{dual}(G)$. Then for
every proper $3$-edge-colouring $\varphi$ of $\widehat{G}'_{v,i}$ there exist
a face $F$ of $\widehat{G}'_{v,i}$ and two distinct edges
$e_1, e_2 \in \partial F$, with neither $e_1$ nor $e_2$ equal to the merged
edge, such that
\begin{enumerate}
\item $\varphi(e_1) = \varphi(e_2)$, and
\item $e_1$, $e_2$, and the merged edge all lie on a common
$\{a, b\}$-Kempe cycle of $\varphi$.
\end{enumerate}
\end{conjecture}
\end{document}