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>
311 lines
10 KiB
Python
311 lines
10 KiB
Python
"""Investigate the conjecture:
|
|
#(Kempe classes of H_t*) <= #(Kempe classes of H_1).
|
|
|
|
For each min-deg-5 triangulation G and each (face, i_red) choice giving a
|
|
valid H_1, vary phi_1 across many proper 3-edge-colorings of H_1 -- so we
|
|
exercise multiple algorithm executions, each potentially producing a
|
|
different H_t*. For each execution, count Kempe classes of H_t* and compare
|
|
to H_1.
|
|
|
|
Reports any case where #classes(H_t*) > #classes(H_1) (counterexample to
|
|
the conjecture).
|
|
|
|
Run with: sage experiments/check_kempe_class_monotone.py
|
|
"""
|
|
from sage.all import Graph
|
|
from sage.graphs.graph_generators import graphs
|
|
import sys
|
|
import time
|
|
|
|
|
|
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)
|
|
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
|
|
return Graph(dual_edges, multiedges=False, loops=False)
|
|
|
|
|
|
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 num_kempe_classes(G):
|
|
edges, colorings = proper_3_edge_colorings(G)
|
|
if not colorings:
|
|
return 0, 0
|
|
n = len(colorings)
|
|
parent = list(range(n))
|
|
|
|
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
|
|
|
|
col_idx = {c: i for i, c in enumerate(colorings)}
|
|
for c_idx, col in enumerate(colorings):
|
|
for pair in [(0, 1), (0, 2), (1, 2)]:
|
|
visited = set()
|
|
for start in range(len(edges)):
|
|
if col[start] not in pair or start in visited:
|
|
continue
|
|
cyc = kempe_cycle(edges, col, start, pair)
|
|
visited |= cyc
|
|
a, b = pair
|
|
new_col = list(col)
|
|
for k in cyc:
|
|
if new_col[k] == a:
|
|
new_col[k] = b
|
|
elif new_col[k] == b:
|
|
new_col[k] = a
|
|
new_col = tuple(new_col)
|
|
if new_col != col and new_col in col_idx:
|
|
union(c_idx, col_idx[new_col])
|
|
classes = len({find(i) for i in range(n)})
|
|
return n, classes
|
|
|
|
|
|
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:
|
|
return None
|
|
if 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, 'A': A,
|
|
'named': {
|
|
'spike': frozenset(spike),
|
|
'side_0': frozenset(side_0),
|
|
'side_1': frozenset(side_1),
|
|
'merged': frozenset(merged),
|
|
},
|
|
}
|
|
|
|
|
|
def find_safe_pentagonal_face(G, protected):
|
|
for face in G.faces():
|
|
if len(face) != 5:
|
|
continue
|
|
boundary = [u for (u, v) in face]
|
|
boundary_edges = [frozenset([u, v]) for (u, v) in face]
|
|
externals = []
|
|
A = []
|
|
ok = True
|
|
for B_k in boundary:
|
|
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
|
if len(outer) != 1:
|
|
ok = False
|
|
break
|
|
externals.append(frozenset([B_k, outer[0]]))
|
|
A.append(outer[0])
|
|
if not ok:
|
|
continue
|
|
if any(e in protected for e in boundary_edges + externals):
|
|
continue
|
|
return boundary, externals, A
|
|
return None
|
|
|
|
|
|
def valid_index(f_vec, A):
|
|
for i in range(5):
|
|
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
|
|
continue
|
|
if len({f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}) != 3:
|
|
continue
|
|
if A[(i + 3) % 5] == A[(i + 4) % 5]:
|
|
continue
|
|
return i
|
|
return None
|
|
|
|
|
|
def run_algorithm_to_completion(H, phi_1_dict, named_step1, vn_start=10000):
|
|
H = H.copy()
|
|
coloring = dict(phi_1_dict)
|
|
E = set(named_step1.values())
|
|
next_vn = vn_start
|
|
while True:
|
|
H.is_planar(set_embedding=True)
|
|
result = find_safe_pentagonal_face(H, E)
|
|
if result is None:
|
|
break
|
|
boundary, externals, A = result
|
|
f_vec = [coloring[e] for e in externals]
|
|
i_t = valid_index(f_vec, A)
|
|
if i_t is None:
|
|
break
|
|
v_n = next_vn
|
|
next_vn += 1
|
|
H_new = H.copy()
|
|
for v in boundary:
|
|
H_new.delete_vertex(v)
|
|
H_new.add_vertex(v_n)
|
|
side_0 = (v_n, A[i_t])
|
|
spike = (v_n, A[(i_t + 1) % 5])
|
|
side_1 = (v_n, A[(i_t + 2) % 5])
|
|
merged = (A[(i_t + 3) % 5], A[(i_t + 4) % 5])
|
|
H_new.add_edges([side_0, spike, side_1, merged])
|
|
if H_new.has_multiple_edges() or H_new.has_loops():
|
|
break
|
|
if not H_new.is_planar(set_embedding=True):
|
|
break
|
|
H = H_new
|
|
coloring = {e: c for e, c in coloring.items()
|
|
if not any(u in boundary for u in e)}
|
|
coloring[frozenset(side_0)] = f_vec[i_t]
|
|
coloring[frozenset(spike)] = f_vec[(i_t + 1) % 5]
|
|
coloring[frozenset(side_1)] = f_vec[(i_t + 2) % 5]
|
|
coloring[frozenset(merged)] = f_vec[(i_t + 3) % 5]
|
|
E |= {frozenset(spike), frozenset(side_0), frozenset(side_1),
|
|
frozenset(merged)}
|
|
return H
|
|
|
|
|
|
def main(max_n=15, time_budget_sec=600):
|
|
start = time.time()
|
|
violations = []
|
|
for n in range(12, max_n + 1):
|
|
if time.time() - start > time_budget_sec:
|
|
print(f"Time budget exhausted at n={n}.")
|
|
break
|
|
print(f"\n=== 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):
|
|
if time.time() - start > time_budget_sec:
|
|
break
|
|
G_copy = G.copy()
|
|
G_copy.is_planar(set_embedding=True)
|
|
D = dual_of(G_copy)
|
|
D.is_planar(set_embedding=True)
|
|
for face in D.faces():
|
|
if len(face) != 5:
|
|
continue
|
|
for i_red in range(5):
|
|
if time.time() - start > time_budget_sec:
|
|
break
|
|
res = apply_reduction(D, face, i_red, 9999)
|
|
if res is None:
|
|
continue
|
|
H_1 = res['H']
|
|
n_col_h1, kc_h1 = num_kempe_classes(H_1)
|
|
if kc_h1 == 0:
|
|
continue
|
|
# enumerate all proper colorings of H_1 to vary phi_1
|
|
edges_1, colorings_1 = proper_3_edge_colorings(H_1)
|
|
# try EVERY phi_1 (might be slow for large H_1)
|
|
max_kc_hf = 0
|
|
h_final_seen = {} # signature -> classes
|
|
for phi_1 in colorings_1:
|
|
phi_1_dict = {frozenset((e[0], e[1])): c
|
|
for e, c in zip(edges_1, phi_1)}
|
|
H_final = run_algorithm_to_completion(
|
|
H_1, phi_1_dict, res['named'])
|
|
# signature = (|V|, |E|, sorted edge tuples)
|
|
sig = (H_final.order(), H_final.size())
|
|
if sig in h_final_seen:
|
|
continue
|
|
_, kc_hf = num_kempe_classes(H_final)
|
|
h_final_seen[sig] = kc_hf
|
|
if kc_hf > max_kc_hf:
|
|
max_kc_hf = kc_hf
|
|
over = max_kc_hf > kc_h1
|
|
flag = "*** OVER ***" if over else "ok"
|
|
print(f" n={n} tri#{tri_idx} face|{len(face)} i_red={i_red}: "
|
|
f"H_1 kc={kc_h1}, max H_t* kc over {len(h_final_seen)} "
|
|
f"distinct outputs = {max_kc_hf} {flag}")
|
|
if over:
|
|
violations.append((n, tri_idx, i_red, kc_h1, max_kc_hf))
|
|
sys.stdout.flush()
|
|
break # first face only
|
|
|
|
print()
|
|
if violations:
|
|
print(f"Counterexamples (#K(H_t*) > #K(H_1)): {len(violations)}")
|
|
for v in violations:
|
|
print(f" {v}")
|
|
else:
|
|
print("No counterexamples found within tested range.")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|