Files
math-research/papers/face_monochromatic_pairs/experiments/search_smaller_counterexample.py
T
didericis 3aec31b3ac face_monochromatic_pairs: search for smallest cubic plane counterexample to Conjecture 5.5
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 <noreply@anthropic.com>
2026-05-25 03:31:48 -04:00

214 lines
7.5 KiB
Python

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