dual_decomposition: Conj 3.6 (face/Kempe witness) and constructive lift
Paper: - Lemmas 3.4 (exactly one match) and 3.5 (all-distinct exists for 4-colourable G) replace the earlier conjecture; both have proofs. - Add Conjecture 3.6: every proper 3-edge-colouring of a counterexample's reduced dual has a face with two same-colour edges that share a Kempe cycle with the merged edge, neither of them being the merged edge. Experiments (all under experiments/): - search_conj_3_6_counterexample.py: finds n=14 tri#1 i_red=0 where the algorithm's phi_t* sits in a Kempe class with no all-distinct colouring (disproves an earlier formulation). - check_kempe_class.py / check_kempe_class_invariance.py / check_kempe_class_monotone.py: Kempe-class counts on H_1 and H_t* for small triangulations; neither monotonicity direction holds. - check_all_distinct_exists.py: even in the conj-3.6 disproof case, H_t* itself admits all-distinct colourings in the *other* Kempe class. - check_constrained_feasibility.py: literal H_t*-interpretation of C1 + K0 + K1 is empirically unsatisfiable (gap in proof strategy noted). - check_conj_face_kempe.py / check_conj_face_kempe_n15.py: test Conj 3.6 on chord-apex+Kempe colourings of reduced duals at n=12, 14, 15; 216/216 colourings on n=14 satisfy the conjecture, others vacuous. - draw_step1_conj36.py: figure showing a Conj 3.6 witness on H_1 with two new vertices on the witness edges and a new red bridge between them. - draw_step1_conj36_recolored.py: same but with the Kempe cycle recoloured alternately from merged so propriety holds. - draw_lift_to_Gprime.py: lifts the modified+recoloured H_1 back to a proper 3-edge-colouring of the modified G' (24+2 vertices, 39 edges, same Tutte layout as figure 3's first graphic so positions line up). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user