Files
math-research/papers/face_monochromatic_pairs/experiments/check_kempe_class.py
T
didericis 41227c6a0f papers: rename folders and retitle
- Main paper: dual_decomposition_minimal_counterexamples/ ->
  face_monochromatic_pairs/. Title is now
  "Face-Monochromatic Pairs and the Four Colour Theorem".
- Companion paper: dual_decomposition_iterated_reduction/ ->
  iterated_reduction_in_reduced_dual/. Title is now
  "An Iterated Reduction in the Reduced Dual". Its prose and bibliography
  cite the parent under the new title.
- Update one absolute sys.path reference inside
  check_conj_face_kempe_n15.py that pointed at the old folder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 15:04:15 -04:00

184 lines
5.6 KiB
Python

"""Check empirically whether all proper 3-edge-colorings of a cubic plane
graph form a single Kempe equivalence class.
For each test graph:
1. enumerate all proper 3-edge-colorings;
2. build a multigraph K whose vertices are the colorings and whose edges
connect pairs related by a single Kempe swap (swapping two colors on
one cycle of the 2-color subgraph);
3. report the number of connected components of K (= number of Kempe
classes).
Test inputs:
- Dodecahedron's reduced dual (16 v, 24 e), built as in
check_dodecahedron_kempe.py;
- The n=14 reduced dual found by search_kempe_property.py (20 v, 30 e).
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
import sys
def all_proper_3_edge_colorings(G):
edges = list(G.edges(labels=False))
n = len(edges)
adj = [[] for _ in range(n)]
for i in range(n):
u, v = edges[i][0], edges[i][1]
for j in range(i):
x, y = edges[j][0], edges[j][1]
if u in (x, y) or v in (x, y):
adj[i].append(j)
adj[j].append(i)
coloring = [-1] * n
results = []
def back(k):
if k == n:
results.append(tuple(coloring))
return
for c in range(3):
if all(coloring[j] != c for j in adj[k]):
coloring[k] = c
back(k + 1)
coloring[k] = -1
back(0)
return edges, results
def kempe_cycles_of(edges, coloring, color_pair):
"""List of connected components (as sorted tuples of edge indices) in
the subgraph of edges with color in color_pair."""
a, b = color_pair
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
in_set = set(in_sub)
visited = set()
components = []
for start in in_sub:
if start in visited:
continue
comp = []
stack = [start]
visited.add(start)
while stack:
cur = stack.pop()
comp.append(cur)
u, v = edges[cur][0], edges[cur][1]
for j in in_set:
if j in visited:
continue
x, y = edges[j][0], edges[j][1]
if u in (x, y) or v in (x, y):
visited.add(j)
stack.append(j)
components.append(tuple(sorted(comp)))
return components
def swap_on_cycle(coloring, cycle_edges, color_pair):
a, b = color_pair
swap = list(coloring)
for i in cycle_edges:
if swap[i] == a:
swap[i] = b
elif swap[i] == b:
swap[i] = a
return tuple(swap)
def kempe_components(G):
edges, colorings = all_proper_3_edge_colorings(G)
print(f" {len(colorings)} proper 3-edge-colorings")
idx = {c: i for i, c in enumerate(colorings)}
parent = list(range(len(colorings)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
for col in colorings:
for a, b in [(0, 1), (0, 2), (1, 2)]:
for cyc in kempe_cycles_of(edges, col, (a, b)):
new_col = swap_on_cycle(col, cyc, (a, b))
if new_col == col:
continue
if new_col in idx:
union(idx[col], idx[new_col])
n_classes = len({find(i) for i in range(len(colorings))})
print(f" Kempe equivalence classes: {n_classes}")
return n_classes
def dodecahedron_reduced_dual():
edges = []
for k in range(5):
edges.append((('b', k), ('c', k)))
edges.append((('b', k), ('c', (k - 1) % 5)))
edges.append((('c', k), ('d', k)))
edges.append((('d', k), ('d', (k + 1) % 5)))
edges.append(('v_n', ('b', 0)))
edges.append(('v_n', ('b', 1)))
edges.append(('v_n', ('b', 2)))
edges.append((('b', 3), ('b', 4)))
return Graph(edges, multiedges=False, loops=False)
def n14_reduced_dual():
# First min-degree-5 plantri triangulation on 14 vertices, reduced at the
# first pentagonal face with i_red = 1 (matches search_kempe_property.py).
G = next(graphs.triangulations(14, minimum_degree=5))
G.is_planar(set_embedding=True)
faces = G.faces()
edge_to_faces = {}
for fi, face in enumerate(faces):
for u, v in face:
edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
D = Graph(dual_edges, multiedges=False, loops=False)
D.is_planar(set_embedding=True)
# use first pentagonal face, i_red = 1
face = next(f for f in D.faces() if len(f) == 5)
boundary = [u for (u, v) in face]
A = []
for B_k in boundary:
outer = [w for w in D.neighbor_iterator(B_k) if w not in boundary]
A.append(outer[0])
H = D.copy()
for v in boundary:
H.delete_vertex(v)
vn = '__v_n__'
H.add_vertex(vn)
H.add_edge(vn, A[1]) # spike
H.add_edge(vn, A[0]) # side_0
H.add_edge(vn, A[2]) # side_1
H.add_edge(A[3], A[4]) # merged
H.is_planar(set_embedding=True)
return H
def main():
print("Dodecahedron's reduced dual:")
G1 = dodecahedron_reduced_dual()
kempe_components(G1)
print()
print("n=14 reduced dual (first plantri triangulation, i_red=1):")
G2 = n14_reduced_dual()
kempe_components(G2)
print()
print("Small sanity check: K_4")
K4 = graphs.CompleteGraph(4)
kempe_components(K4)
if __name__ == '__main__':
main()