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>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user