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>
226 lines
7.6 KiB
Python
226 lines
7.6 KiB
Python
"""Search for a proper 3-edge-coloring of a reduced dual satisfying:
|
|
|
|
(i) color(spike) == color(merged)
|
|
(ii) the {c_spike, c_side_0}-Kempe cycle through the spike contains
|
|
side_0 AND merged
|
|
(iii) the {c_spike, c_side_1}-Kempe cycle through the spike contains
|
|
side_1 AND merged
|
|
|
|
Iterates over all min-degree-5 simple planar triangulations on n vertices
|
|
(via plantri through Sage), then every pentagonal face of the dual G', then
|
|
every index i in {0,...,4} for the reduced-dual construction, then every
|
|
proper 3-edge-coloring of the resulting reduced dual. Stops on the first hit.
|
|
|
|
The icosahedron (n = 12) is the only n = 12 triangulation with min degree 5
|
|
and was already known to fail (see check_dodecahedron_kempe.py).
|
|
|
|
Run with:
|
|
sage experiments/search_kempe_property.py
|
|
"""
|
|
from sage.all import Graph
|
|
from sage.graphs.graph_generators import graphs
|
|
import sys
|
|
import time
|
|
|
|
|
|
def dual_of(G):
|
|
"""Combinatorial dual of a planar G with the embedding set on G."""
|
|
faces = G.faces()
|
|
edge_to_faces = {}
|
|
for fi, face in enumerate(faces):
|
|
for u, v in face:
|
|
e = frozenset((u, v))
|
|
edge_to_faces.setdefault(e, []).append(fi)
|
|
dual_edges = []
|
|
for e, fs in edge_to_faces.items():
|
|
if len(fs) == 2:
|
|
dual_edges.append((fs[0], fs[1]))
|
|
return Graph(dual_edges, multiedges=False, loops=False)
|
|
|
|
|
|
def apply_reduction(G, face, i):
|
|
"""Apply Definition 2.1 at the pentagonal `face` (list of directed edges)
|
|
with index i, returning (H, named) or (None, None) if the construction
|
|
breaks."""
|
|
boundary = [u for (u, v) in face]
|
|
if len(set(boundary)) != 5:
|
|
return None, 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, None
|
|
A.append(outer[0])
|
|
if len(set(A)) != 5:
|
|
return None, None
|
|
H = G.copy()
|
|
for v in boundary:
|
|
H.delete_vertex(v)
|
|
v_n = '__v_n__'
|
|
H.add_vertex(v_n)
|
|
side_0 = (v_n, A[i % 5])
|
|
spike = (v_n, A[(i + 1) % 5])
|
|
side_1 = (v_n, A[(i + 2) % 5])
|
|
merged = (A[(i + 3) % 5], A[(i + 4) % 5])
|
|
# If merged would be a self-loop or duplicate of an existing edge, skip
|
|
if A[(i + 3) % 5] == A[(i + 4) % 5]:
|
|
return None, None
|
|
H.add_edges([side_0, spike, side_1, merged])
|
|
if H.has_multiple_edges() or H.has_loops():
|
|
return None, None
|
|
if not H.is_planar(set_embedding=True):
|
|
return None, None
|
|
# Cubic check
|
|
if not all(H.degree(v) == 3 for v in H.vertex_iterator()):
|
|
return None, None
|
|
named = {
|
|
'spike': frozenset(spike),
|
|
'side_0': frozenset(side_0),
|
|
'side_1': frozenset(side_1),
|
|
'merged': frozenset(merged),
|
|
}
|
|
return H, named
|
|
|
|
|
|
def proper_3_edge_colorings(G):
|
|
edges = list(G.edges(labels=False))
|
|
n_edges = len(edges)
|
|
adj = [[] for _ in range(n_edges)]
|
|
for i in range(n_edges):
|
|
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_edges
|
|
|
|
def back(k):
|
|
if k == n_edges:
|
|
yield tuple(coloring)
|
|
return
|
|
for c in range(3):
|
|
if all(coloring[j] != c for j in adj[k]):
|
|
coloring[k] = c
|
|
yield from back(k + 1)
|
|
coloring[k] = -1
|
|
|
|
return edges, back(0)
|
|
|
|
|
|
def kempe_cycle(edges, coloring, start_idx, color_pair):
|
|
a, b = color_pair
|
|
if coloring[start_idx] not in (a, b):
|
|
return None
|
|
in_sub = [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 matches(edges, col, named_idx):
|
|
c_spike = col[named_idx['spike']]
|
|
c_merged = col[named_idx['merged']]
|
|
if c_spike != c_merged:
|
|
return False
|
|
c_s0 = col[named_idx['side_0']]
|
|
c_s1 = col[named_idx['side_1']]
|
|
kc0 = kempe_cycle(edges, col, named_idx['spike'], (c_spike, c_s0))
|
|
if named_idx['side_0'] not in kc0 or named_idx['merged'] not in kc0:
|
|
return False
|
|
kc1 = kempe_cycle(edges, col, named_idx['spike'], (c_spike, c_s1))
|
|
if named_idx['side_1'] not in kc1 or named_idx['merged'] not in kc1:
|
|
return False
|
|
return True
|
|
|
|
|
|
def report(edges, col, named_idx, named, info):
|
|
print()
|
|
print("*** MATCH FOUND ***")
|
|
for k, v in info.items():
|
|
print(f" {k} = {v}")
|
|
print("Named edges and their colours:")
|
|
for role in ('spike', 'side_0', 'side_1', 'merged'):
|
|
e = edges[named_idx[role]]
|
|
print(f" {role:7s}: edge {tuple(e)}, colour {col[named_idx[role]]}")
|
|
print(f"Full coloring (indexed by edge order):")
|
|
for i, e in enumerate(edges):
|
|
print(f" [{col[i]}] {tuple(e)}")
|
|
|
|
|
|
def search(max_n=16, time_budget_sec=600):
|
|
start = time.time()
|
|
n = 12
|
|
while n <= max_n:
|
|
elapsed = time.time() - start
|
|
if elapsed > time_budget_sec:
|
|
print(f"\nTime budget {time_budget_sec}s exhausted at n = {n}.")
|
|
return
|
|
print(f"\n=== n = {n} === (elapsed {elapsed:.1f}s)")
|
|
tcount = 0
|
|
total_reductions = 0
|
|
total_colorings = 0
|
|
try:
|
|
it = graphs.triangulations(n, minimum_degree=5)
|
|
except Exception as ex:
|
|
print(f" cannot enumerate triangulations: {ex}")
|
|
n += 1
|
|
continue
|
|
for G in it:
|
|
tcount += 1
|
|
if not G.is_planar(set_embedding=True):
|
|
continue
|
|
D = dual_of(G)
|
|
if not D.is_planar(set_embedding=True):
|
|
continue
|
|
faces_D = D.faces()
|
|
pentagonal = [f for f in faces_D if len(f) == 5]
|
|
for face in pentagonal:
|
|
for i_red in range(5):
|
|
H, named = apply_reduction(D, face, i_red)
|
|
if H is None:
|
|
continue
|
|
total_reductions += 1
|
|
edges, gen = proper_3_edge_colorings(H)
|
|
# Find indices of named edges
|
|
named_idx = {}
|
|
for ii, e in enumerate(edges):
|
|
es = frozenset((e[0], e[1]))
|
|
for role, ns in named.items():
|
|
if es == ns:
|
|
named_idx[role] = ii
|
|
if len(named_idx) != 4:
|
|
continue
|
|
for col in gen:
|
|
total_colorings += 1
|
|
if matches(edges, col, named_idx):
|
|
report(edges, col, named_idx, named, {
|
|
'n': n,
|
|
'triangulation_index': tcount,
|
|
'face_size': len(face),
|
|
'i_red': i_red,
|
|
'reduced_dual_V': H.order(),
|
|
'reduced_dual_E': H.size(),
|
|
})
|
|
return
|
|
sys.stdout.flush()
|
|
print(f" n = {n}: {tcount} triangulation(s), "
|
|
f"{total_reductions} reductions, "
|
|
f"{total_colorings} colorings; no hit.")
|
|
n += 1
|
|
print(f"\nSearch exhausted up to n = {max_n}.")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
search()
|