3aec31b3ac
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>
214 lines
7.5 KiB
Python
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()
|