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>
201 lines
7.4 KiB
Python
201 lines
7.4 KiB
Python
"""Test Conjecture 3.6: for every reduced dual of a minimal counterexample,
|
|
every proper 3-edge-coloring has a face with two same-color edges that share
|
|
a Kempe cycle with the merged edge.
|
|
|
|
We can't run on actual counterexamples (none exist by the 4CT). Instead we
|
|
test on the structural surrogates: proper 3-edge-colorings of reduced duals
|
|
that *satisfy* chord-apex (Lemma 2.6) and the Kempe-cycle condition
|
|
(Lemma 2.7). These are the kinds of colorings a counterexample's reduced
|
|
dual would be forced to admit.
|
|
|
|
For each such (G, face, i_red, phi), check whether some face F of H_1 and
|
|
some pair of edges (e1, e2) in partial F satisfy:
|
|
- phi(e1) = phi(e2)
|
|
- e1, e2, and merged all lie on a common {a,b}-Kempe cycle.
|
|
|
|
If conjecture FAILS for some coloring, we have empirical evidence against.
|
|
If every coloring passes, evidence in favour.
|
|
|
|
Run with: sage experiments/check_conj_face_kempe.py
|
|
"""
|
|
from sage.all import Graph
|
|
from sage.graphs.graph_generators import graphs
|
|
|
|
|
|
def dual_of(G):
|
|
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)
|
|
return Graph(
|
|
[(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
|
|
multiedges=False, loops=False)
|
|
|
|
|
|
def apply_reduction(G, face, i, v_n_label):
|
|
boundary = [u for (u, v) in face]
|
|
if len(set(boundary)) != 5: return None
|
|
A = []
|
|
for B_k in boundary:
|
|
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
|
if len(outer) != 1: return None
|
|
A.append(outer[0])
|
|
if len(set(A)) != 5 or A[(i+3) % 5] == A[(i+4) % 5]: return None
|
|
H = G.copy()
|
|
for v in boundary: H.delete_vertex(v)
|
|
H.add_vertex(v_n_label)
|
|
side_0 = (v_n_label, A[i])
|
|
spike = (v_n_label, A[(i+1) % 5])
|
|
side_1 = (v_n_label, A[(i+2) % 5])
|
|
merged = (A[(i+3) % 5], A[(i+4) % 5])
|
|
H.add_edges([side_0, spike, side_1, merged])
|
|
if H.has_multiple_edges() or H.has_loops(): return None
|
|
if not H.is_planar(set_embedding=True): return None
|
|
if not all(H.degree(v) == 3 for v in H.vertex_iterator()): return None
|
|
return {
|
|
'H': H,
|
|
'named': {
|
|
'spike': frozenset(spike), 'side_0': frozenset(side_0),
|
|
'side_1': frozenset(side_1), 'merged': frozenset(merged),
|
|
},
|
|
}
|
|
|
|
|
|
def 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_cycle(edges, coloring, start_idx, color_pair):
|
|
a, b = color_pair
|
|
if coloring[start_idx] not in (a, b):
|
|
return set()
|
|
in_sub = set(i for i in range(len(edges)) if coloring[i] in (a, b))
|
|
visited = {start_idx}
|
|
stack = [start_idx]
|
|
while stack:
|
|
cur = stack.pop()
|
|
u, v = edges[cur][0], edges[cur][1]
|
|
for j in in_sub:
|
|
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)
|
|
return visited
|
|
|
|
|
|
def edge_idx(edges, e_frozen):
|
|
for i, e in enumerate(edges):
|
|
if frozenset((e[0], e[1])) == e_frozen:
|
|
return i
|
|
return None
|
|
|
|
|
|
def matches_chord_apex_kempe(edges, col, named):
|
|
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
|
|
if any(v is None for v in idx.values()): return False
|
|
c_spike = col[idx['spike']]
|
|
c_merged = col[idx['merged']]
|
|
if c_spike != c_merged: return False
|
|
c_s0 = col[idx['side_0']]; c_s1 = col[idx['side_1']]
|
|
kc0 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s0))
|
|
if idx['side_0'] not in kc0 or idx['merged'] not in kc0: return False
|
|
kc1 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s1))
|
|
if idx['side_1'] not in kc1 or idx['merged'] not in kc1: return False
|
|
return True
|
|
|
|
|
|
def conjecture_holds_for(H, edges, col, named):
|
|
"""Returns (F, e1, e2, kc) if some face F of H has two same-colour edges
|
|
(e1, e2), neither equal to merged, both on a Kempe cycle kc through
|
|
merged, AND exactly one edge of partial F lies between e1 and e2 along
|
|
one of the two arcs of partial F. Else None."""
|
|
merged_idx = edge_idx(edges, named['merged'])
|
|
c_merged = col[merged_idx]
|
|
kempe_cycles = []
|
|
for c_prime in range(3):
|
|
if c_prime == c_merged: continue
|
|
kc = kempe_cycle(edges, col, merged_idx, (c_merged, c_prime))
|
|
kempe_cycles.append(kc)
|
|
for face in H.faces():
|
|
face_edge_indices = []
|
|
for u, v in face:
|
|
ei = edge_idx(edges, frozenset((u, v)))
|
|
if ei is not None:
|
|
face_edge_indices.append(ei)
|
|
n_face = len(face_edge_indices)
|
|
for i in range(n_face):
|
|
for j in range(i + 1, n_face):
|
|
e1, e2 = face_edge_indices[i], face_edge_indices[j]
|
|
if e1 == merged_idx or e2 == merged_idx: continue
|
|
if col[e1] != col[e2]: continue
|
|
# exactly one edge between e1 and e2 in one arc
|
|
gap_a = (j - i - 1)
|
|
gap_b = (n_face - 2 - gap_a)
|
|
if gap_a != 1 and gap_b != 1:
|
|
continue
|
|
for kc in kempe_cycles:
|
|
if e1 in kc and e2 in kc:
|
|
return face, e1, e2, kc
|
|
return None
|
|
|
|
|
|
def main():
|
|
for n in [12, 14, 15]:
|
|
print(f"=== n = {n} ===")
|
|
try:
|
|
triangulations = list(graphs.triangulations(n, minimum_degree=5))
|
|
except Exception as ex:
|
|
print(f" cannot enumerate: {ex}"); continue
|
|
for tri_idx, G in enumerate(triangulations, start=1):
|
|
G.is_planar(set_embedding=True)
|
|
D = dual_of(G); D.is_planar(set_embedding=True)
|
|
for face_no, face in enumerate(
|
|
f for f in D.faces() if len(f) == 5):
|
|
for i_red in range(5):
|
|
res = apply_reduction(D, face, i_red, 9999)
|
|
if res is None: continue
|
|
H = res['H']; named = res['named']
|
|
H.is_planar(set_embedding=True)
|
|
edges, colorings = proper_3_edge_colorings(H)
|
|
cand = [c for c in colorings
|
|
if matches_chord_apex_kempe(edges, c, named)]
|
|
if not cand: continue
|
|
n_pass = sum(1 for c in cand
|
|
if conjecture_holds_for(H, edges, c, named)
|
|
is not None)
|
|
n_fail = len(cand) - n_pass
|
|
status = "PASSED" if n_fail == 0 else "*** FAILED ***"
|
|
print(f" n={n} tri#{tri_idx} face#{face_no} i_red={i_red}: "
|
|
f"{len(cand)} chord-apex+Kempe colorings; "
|
|
f"{n_pass} satisfy conjecture, {n_fail} fail. "
|
|
f"{status}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|