From 3aec31b3ac4d05712dda0e1bce7733d0715e11d9 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 03:31:48 -0400 Subject: [PATCH] face_monochromatic_pairs: search for smallest cubic plane counterexample to Conjecture 5.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit experiments/search_smaller_counterexample.py enumerates 3-connected cubic planar graphs via graphs.planar_graphs(n, min_deg=3, min_conn=2) (filtering to cubic), then for each graph tries every proper 3-edge-colouring (backtracking with symmetry-break on first edge), computes h_φ via the CW rotation from sage's planar embedding, and checks whether some pair of intersecting Kempe cycles K_{a,b} and K_{a,c} are both constant-Heawood. Results (up to n=10 in initial run): n= 4: K_4 itself. Coloring (1,2)=red, (3,4)=red, (1,3)=blue, (2,4)=blue, (1,4)=green, (2,3)=green; sage's CW embedding gives h_φ ≡ -1 on all 4 vertices. K_{red,blue} = 4-cycle 1-2-4-3 and K_{red,green} = 4-cycle 1-2-3-4 share both red edges; both constant. n= 6: no counterexample (only the triangular prism). n= 8: a 12-edge cubic planar graph (graph6 G}GOW[) on 8 vertices. Both Kempe cycles are 8-cycles visiting every vertex. n=10: 8 cubic planar graphs checked, no counterexample. So K_4 is the smallest counterexample to Conjecture 5.5 as stated, but both K_4 and the n=8 example are structurally trivial: K_0 and K_1 jointly cover V(H). The user's 40-vertex counterexample (paper Figure) is the smallest non-trivial example found so far, with 24 vertices outside V(K_0) ∪ V(K_1). Co-Authored-By: Claude Opus 4.7 --- .../search_smaller_counterexample.py | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 papers/face_monochromatic_pairs/experiments/search_smaller_counterexample.py diff --git a/papers/face_monochromatic_pairs/experiments/search_smaller_counterexample.py b/papers/face_monochromatic_pairs/experiments/search_smaller_counterexample.py new file mode 100644 index 0000000..f1ac02b --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/search_smaller_counterexample.py @@ -0,0 +1,213 @@ +"""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 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:] + + 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'})\n") + + first_found = None + for n in range(4, max_n + 1, 2): # cubic requires n even + start = time.time() + count = 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 + 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: + print(f"n={n:>3}: checked {count} graphs, no counterexample " + f"[{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()