"""Search for the smallest cubic plane graph admitting a proper 3-edge-colouring on which two intersecting Kempe cycles both have constant Heawood number. The known counterexample (Figure \\ref{fig:no-two-constant-kempe-counterexample} in paper.tex) has 40 vertices. This script searches small cubic planar graphs to see how small a counterexample can be. For each n in [4, max_n]: - enumerate all 3-connected cubic planar graphs on n vertices via sage's graphs.planar_graphs() with degree restrictions; - for each graph, set a planar embedding and enumerate all proper 3-edge-colourings via backtracking; - for each colouring, compute h_φ at every vertex from the CW rotation; - try every colour-a edge as the candidate shared edge between K_{a,b} and K_{a,c}; trace both Kempe cycles and check whether h_φ is constant on V(K_{a,b}) and on V(K_{a,c}) simultaneously. If a counterexample is found at some n < 40, print the edge list + colouring + cycle data + Heawood values and stop searching that n (but continue searching larger n if --all is passed). Run with: sage experiments/search_smaller_counterexample.py # default max_n=18 sage experiments/search_smaller_counterexample.py 24 # specify max_n sage experiments/search_smaller_counterexample.py 24 --all # find at every n """ import sys import time from sage.all import Graph from sage.graphs.graph_generators import graphs def edge_key(u, v): return (u, v) if u <= v else (v, u) def all_proper_3_edge_colourings(edges, vertex_edges): """Yield every proper 3-edge-colouring of the given edge list as a tuple indexed by the edges list.""" n = len(edges) colours = [None] * n # Symmetry break: first edge always gets colour 0. def backtrack(i): if i == n: yield tuple(colours) return u, v = edges[i] used = set() for j in vertex_edges[u]: if j != i and colours[j] is not None: used.add(colours[j]) for j in vertex_edges[v]: if j != i and colours[j] is not None: used.add(colours[j]) c_range = (0,) if i == 0 else range(3) for c in c_range: if c in used: continue colours[i] = c yield from backtrack(i + 1) colours[i] = None yield from backtrack(0) def heawood_numbers(G, col_of_edge): """Return {v: ±1} from the CW rotation around each vertex of the planar embedding. col_of_edge: dict edge_key -> colour ∈ {0,1,2}.""" emb = G.get_embedding() h = {} for v in G.vertex_iterator(): cols = [col_of_edge[edge_key(v, u)] for u in emb[v]] # cyclic class i0 = cols.index(0) rot = cols[i0:] + cols[:i0] if rot == [0, 1, 2]: h[v] = +1 elif rot == [0, 2, 1]: h[v] = -1 else: return None return h def trace_kempe(G, col_of_edge, start_edge, two_colours): """Trace the Kempe cycle in colours `two_colours` containing start_edge. Returns the vertex sequence (length = cycle length).""" target = set(two_colours) u0, v0 = start_edge walk = [u0, v0] bound = 4 * G.order() + 4 while True: cur, prev = walk[-1], walk[-2] nxt = None for u in G.neighbors(cur): if u == prev: continue if col_of_edge[edge_key(cur, u)] in target: nxt = u break if nxt is None: return None if nxt == walk[0]: return walk walk.append(nxt) if len(walk) > bound: return None def check_graph(G): """Return a counterexample (col, K0, K1, h_K0, h_K1, a, b, c) for G, or None.""" if not G.is_planar(set_embedding=True): return None G.is_planar(set_embedding=True) # ensure embedding is set edges = sorted([edge_key(u, v) for (u, v) in G.edge_iterator(labels=False)]) edge_idx = {e: i for i, e in enumerate(edges)} vertex_edges = { v: [edge_idx[edge_key(v, u)] for u in G.neighbors(v)] for v in G.vertex_iterator() } for col in all_proper_3_edge_colourings(edges, vertex_edges): col_of_edge = {edges[i]: col[i] for i in range(len(edges))} h = heawood_numbers(G, col_of_edge) if h is None: continue # For each candidate shared colour a and shared edge: for a in range(3): other = [c for c in range(3) if c != a] b, c = other for e_i, e in enumerate(edges): if col[e_i] != a: continue K0 = trace_kempe(G, col_of_edge, e, (a, b)) K1 = trace_kempe(G, col_of_edge, e, (a, c)) if K0 is None or K1 is None: continue h0 = {h[v] for v in K0} h1 = {h[v] for v in K1} if len(h0) == 1 and len(h1) == 1: return (col, K0, K1, next(iter(h0)), next(iter(h1)), a, b, c, e) return None def min_face_length(G): """Min face length of G in its current planar embedding.""" return min(len(f) for f in G.faces()) def main(): args = [a for a in sys.argv[1:] if not a.startswith('--')] max_n = int(args[0]) if args else 18 flag_all = '--all' in sys.argv[1:] # Parse --min-face N min_face = None for i, a in enumerate(sys.argv[1:]): if a == '--min-face' and i + 1 < len(sys.argv) - 1: min_face = int(sys.argv[i + 2]) break if a.startswith('--min-face='): min_face = int(a.split('=', 1)[1]) break print(f"Searching for cubic plane graph counterexamples to " f"Conjecture 5.5, n in [4, {max_n}] " f"({'continuing past first hit' if flag_all else 'stopping at first hit'})" + (f", min face length >= {min_face}" if min_face else "") + "\n") # If filtering by min face length, skip small n where impossible. # For all-faces-length->=L cubic plane: V - E + F = 2, E = 3V/2, # sum face lengths = 3V, F = V/2 + 2, so min sum = L*(V/2 + 2) # <= 3V gives V >= 2L*(L-3)/(3-L/2)... let's just set V >= 20 for L=5. if min_face is not None and min_face >= 5: n_start = max(4, 20) else: n_start = 4 first_found = None for n in range(n_start, max_n + 1, 2): # cubic requires n even start = time.time() count = 0 skipped = 0 found = None try: gen = graphs.planar_graphs( n, minimum_degree=3, minimum_connectivity=2, ) except Exception as ex: print(f"n={n:>3}: cannot enumerate ({ex})") continue for G in gen: if max(G.degree()) != 3: continue # not cubic if min_face is not None: G.is_planar(set_embedding=True) if min_face_length(G) < min_face: skipped += 1 continue count += 1 res = check_graph(G) if res is not None: found = (G.copy(), res) if not flag_all: break elapsed = time.time() - start if found is None: extra = (f", skipped {skipped} due to small face" if min_face is not None else "") print(f"n={n:>3}: checked {count} graphs, no counterexample" f"{extra} [{elapsed:.1f}s]") else: G, (col, K0, K1, h0, h1, a, b, c, e) = found colour_name = {0: 'red', 1: 'blue', 2: 'green'} print(f"n={n:>3}: COUNTEREXAMPLE in graph #{count} [{elapsed:.1f}s]") print(f" edges = {sorted(G.edges(labels=False))}") print(f" colouring (sorted-edge order): {col}") print(f" shared colour a = {colour_name[a]} ({a}), " f"shared edge {e}") print(f" K_{{a,b}} = K_{{{colour_name[a]},{colour_name[b]}}} " f"= {K0} (h = {h0:+d}, |V| = {len(K0)})") print(f" K_{{a,c}} = K_{{{colour_name[a]},{colour_name[c]}}} " f"= {K1} (h = {h1:+d}, |V| = {len(K1)})") print(f" canonical graph6 = " f"{G.canonical_label().graph6_string()}") if first_found is None: first_found = (n, G, col, K0, K1, h0, h1, a, b, c, e) if not flag_all: break sys.stdout.flush() if first_found is not None: n, *_ = first_found print(f"\nSmallest counterexample found at n = {n}.") else: print(f"\nNo counterexample found for n ≤ {max_n}.") if __name__ == '__main__': main()