41227c6a0f
- Main paper: dual_decomposition_minimal_counterexamples/ -> face_monochromatic_pairs/. Title is now "Face-Monochromatic Pairs and the Four Colour Theorem". - Companion paper: dual_decomposition_iterated_reduction/ -> iterated_reduction_in_reduced_dual/. Title is now "An Iterated Reduction in the Reduced Dual". Its prose and bibliography cite the parent under the new title. - Update one absolute sys.path reference inside check_conj_face_kempe_n15.py that pointed at the old folder. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
239 lines
9.2 KiB
Python
239 lines
9.2 KiB
Python
"""Iterated reduced-dual reduction with protected edges (Sage).
|
|
|
|
Implements Algorithm 3.1 from paper.tex:
|
|
|
|
0. G' = dual of G (a triangulation conjectured to be a minimal
|
|
counterexample).
|
|
1. Apply Definition 2.1 to G' at a pentagonal face to get H_1; find a
|
|
proper 3-edge-colouring phi_1 of H_1.
|
|
2. Initialise protected edge set E := {spike, side-0, side-1, merged} from
|
|
step 1.
|
|
3. Iterate: find a pentagonal face of H_{t-1} whose 10 incident edges are
|
|
disjoint from E, pick a valid index i_t (Lemma 2.4), apply
|
|
Definition 2.1, extend phi_{t-1} to phi_t, and add the four new named
|
|
edges to E.
|
|
4. Terminate when no safe pentagonal face exists.
|
|
|
|
We run on the icosahedron-dodecahedron pair as a concrete trace; the
|
|
icosahedron is 4-colourable, so the dodecahedron is 3-edge-colourable and
|
|
the algorithm terminates combinatorially.
|
|
|
|
Run with:
|
|
sage experiments/iterated_reduction.py
|
|
"""
|
|
from sage.all import Graph
|
|
from sage.graphs.graph_coloring import edge_coloring
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build G' = dodecahedron with the same naming as experiments/reduced_dual.py:
|
|
# vertex families a (inner pentagon, will play the role of partial F_v's
|
|
# boundary vertices for the first reduction), b, c, d.
|
|
# ---------------------------------------------------------------------------
|
|
def build_dodecahedron():
|
|
edges = []
|
|
for i in range(5):
|
|
edges.append((('a', i), ('a', (i + 1) % 5))) # inner pentagon
|
|
edges.append((('a', i), ('b', i))) # spoke a-b
|
|
edges.append((('b', i), ('c', i))) # b-c
|
|
edges.append((('b', i), ('c', (i - 1) % 5))) # b-c (other side)
|
|
edges.append((('c', i), ('d', i))) # spoke c-d
|
|
edges.append((('d', i), ('d', (i + 1) % 5))) # outer pentagon
|
|
G = Graph(edges, multiedges=False, loops=False)
|
|
assert G.is_planar(set_embedding=True), "dodecahedron not planar?"
|
|
return G
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3-edge-colour a cubic plane graph; return None if not 3-edge-colourable.
|
|
# ---------------------------------------------------------------------------
|
|
def proper_3_coloring(G):
|
|
classes = edge_coloring(G, value_only=False)
|
|
if len(classes) > 3:
|
|
return None
|
|
out = {}
|
|
for k, edge_list in enumerate(classes):
|
|
for u, v in edge_list:
|
|
out[frozenset([u, v])] = k
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Face search: pentagonal face whose 10 incident edges are outside `protected`.
|
|
# Returns (boundary, externals, A) or None.
|
|
# ---------------------------------------------------------------------------
|
|
def find_safe_pentagonal_face(G, protected):
|
|
for face in G.faces():
|
|
if len(face) != 5:
|
|
continue
|
|
boundary = [u for (u, v) in face]
|
|
boundary_edges = [frozenset([u, v]) for (u, v) in face]
|
|
# the external edge at each boundary vertex (the one not on the face)
|
|
externals = []
|
|
A = []
|
|
for B_k in boundary:
|
|
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
|
if len(outer) != 1:
|
|
# In a cubic graph each face-boundary vertex has exactly one
|
|
# external edge; otherwise something is off.
|
|
break
|
|
externals.append(frozenset([B_k, outer[0]]))
|
|
A.append(outer[0])
|
|
else:
|
|
if not any(e in protected for e in boundary_edges + externals):
|
|
return boundary, externals, A
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lemma 2.4: any proper 3-edge-colouring forces the external vector at a
|
|
# pentagonal face to have shape (a, b, c, c, c) up to cyclic rotation. The
|
|
# valid index i for the reduction is one where positions i+3, i+4 host the
|
|
# doubled colour and positions i, i+1, i+2 host three distinct colours.
|
|
# ---------------------------------------------------------------------------
|
|
def valid_indices(f_vec):
|
|
out = []
|
|
for i in range(5):
|
|
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
|
|
continue
|
|
triple = {f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}
|
|
if len(triple) == 3:
|
|
out.append(i)
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Apply Definition 2.1 at (F, i): delete the 5 boundary vertices, add v_n
|
|
# connected to A[i], A[i+1], A[i+2], add the chord A[i+3]-A[i+4]. Extend the
|
|
# colouring: every surviving edge keeps its colour; each new edge takes the
|
|
# colour of the deleted external at the same A_k (the unique third colour at
|
|
# A_k under the surviving edges).
|
|
# ---------------------------------------------------------------------------
|
|
def apply_reduction(G, boundary, externals, A, coloring, i, v_n_label):
|
|
G_new = G.copy()
|
|
for v in boundary:
|
|
G_new.delete_vertex(v)
|
|
G_new.add_vertex(v_n_label)
|
|
side_0 = (v_n_label, A[i])
|
|
spike = (v_n_label, A[(i + 1) % 5])
|
|
side_1 = (v_n_label, A[(i + 2) % 5])
|
|
merged = (A[(i + 3) % 5], A[(i + 4) % 5])
|
|
G_new.add_edges([side_0, spike, side_1, merged])
|
|
assert G_new.is_planar(set_embedding=True), "reduction broke planarity"
|
|
|
|
coloring_new = {
|
|
e: c for e, c in coloring.items() if not any(u in boundary for u in e)
|
|
}
|
|
coloring_new[frozenset(side_0)] = coloring[externals[i]]
|
|
coloring_new[frozenset(spike)] = coloring[externals[(i + 1) % 5]]
|
|
coloring_new[frozenset(side_1)] = coloring[externals[(i + 2) % 5]]
|
|
coloring_new[frozenset(merged)] = coloring[externals[(i + 3) % 5]]
|
|
|
|
named = {
|
|
'spike': frozenset(spike),
|
|
'side_0': frozenset(side_0),
|
|
'side_1': frozenset(side_1),
|
|
'merged': frozenset(merged),
|
|
}
|
|
return G_new, coloring_new, named
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sanity check: verify that a coloring is proper (each vertex sees 3 colours).
|
|
# ---------------------------------------------------------------------------
|
|
def is_proper_3_coloring(G, coloring):
|
|
for v in G.vertex_iterator():
|
|
seen = set()
|
|
for u in G.neighbor_iterator(v):
|
|
seen.add(coloring[frozenset([u, v])])
|
|
if len(seen) != G.degree(v):
|
|
return False
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main driver.
|
|
# ---------------------------------------------------------------------------
|
|
def run_algorithm(max_iterations=50, verbose=True):
|
|
if verbose:
|
|
print("STEP 0: G' = dodecahedron (dual of the icosahedron)")
|
|
G_prime = build_dodecahedron()
|
|
if verbose:
|
|
print(f" |V(G')| = {G_prime.order()}, |E(G')| = {G_prime.size()}")
|
|
|
|
# ---- STEP 1: apply Definition 2.1 to G' at the inner pentagon, i_1 = 0
|
|
face_data = find_safe_pentagonal_face(G_prime, set())
|
|
if face_data is None:
|
|
print(" No pentagonal face in G'.")
|
|
return
|
|
boundary, externals, A = face_data
|
|
|
|
H = G_prime.copy()
|
|
for v in boundary:
|
|
H.delete_vertex(v)
|
|
v_n_label = ('v_n', 1)
|
|
H.add_vertex(v_n_label)
|
|
side_0 = (v_n_label, A[0])
|
|
spike = (v_n_label, A[1])
|
|
side_1 = (v_n_label, A[2])
|
|
merged = (A[3], A[4])
|
|
H.add_edges([side_0, spike, side_1, merged])
|
|
assert H.is_planar(set_embedding=True)
|
|
|
|
coloring = proper_3_coloring(H)
|
|
if coloring is None:
|
|
print(" H_1 is not 3-edge-colourable; algorithm halts.")
|
|
return
|
|
assert is_proper_3_coloring(H, coloring)
|
|
if verbose:
|
|
print(f"STEP 1: H_1 = reduced dual at the first face, i_1 = 0")
|
|
print(f" |V(H_1)| = {H.order()}, |E(H_1)| = {H.size()}; "
|
|
f"3-edge-coloured.")
|
|
|
|
# ---- STEP 2: initialise the protected set
|
|
E = {
|
|
frozenset(spike),
|
|
frozenset(side_0),
|
|
frozenset(side_1),
|
|
frozenset(merged),
|
|
}
|
|
if verbose:
|
|
print(f"STEP 2: |E_protected| = {len(E)}")
|
|
|
|
# ---- STEP 3-4: iterate
|
|
for t in range(2, max_iterations + 1):
|
|
face_data = find_safe_pentagonal_face(H, E)
|
|
if face_data is None:
|
|
if verbose:
|
|
print(f"\nTerminated at iteration t = {t}: "
|
|
f"no pentagonal face avoids E.")
|
|
break
|
|
boundary, externals, A = face_data
|
|
f_vec = [coloring[e] for e in externals]
|
|
choices = valid_indices(f_vec)
|
|
if not choices:
|
|
print(f" iter t = {t}: f-vector {f_vec} has no valid index "
|
|
f"(Lemma 2.4 should preclude this --- bug!)")
|
|
break
|
|
i_t = choices[0]
|
|
v_n_label = ('v_n', t)
|
|
H, coloring, named = apply_reduction(
|
|
H, boundary, externals, A, coloring, i_t, v_n_label)
|
|
assert is_proper_3_coloring(H, coloring)
|
|
E |= set(named.values())
|
|
if verbose:
|
|
print(f" iter t = {t:>2}: f = {f_vec}, chose i_t = {i_t}; "
|
|
f"|V| = {H.order():>2}, |E_graph| = {H.size():>2}, "
|
|
f"|E_protected| = {len(E):>2}")
|
|
else:
|
|
if verbose:
|
|
print(f"\nReached max_iterations = {max_iterations}.")
|
|
|
|
if verbose:
|
|
print(f"\nFinal H: |V| = {H.order()}, |E| = {H.size()}, "
|
|
f"|E_protected| = {len(E)}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run_algorithm()
|