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