41227c6a0f
- 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>
184 lines
5.6 KiB
Python
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()
|