"""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()