papers: rename folders and retitle
- 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>
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
"""Probe: even when phi_t* is in a Kempe class with no all-distinct, does
|
||||
H_t* itself admit an all-distinct coloring in SOME (other) Kempe class?
|
||||
|
||||
Reuses the Conj 3.6 counterexample: n=14, tri#1, i_red=0, phi_1 from the
|
||||
chord-apex+Kempe colorings. Enumerates every proper 3-edge-coloring of
|
||||
H_t*, marks the Kempe classes, and for each class reports whether it
|
||||
contains an all-distinct coloring.
|
||||
|
||||
Run with: sage experiments/check_all_distinct_exists.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
import sys
|
||||
|
||||
|
||||
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 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 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)
|
||||
all_named = [named_step1]
|
||||
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]
|
||||
named = {
|
||||
'spike': frozenset(spike),
|
||||
'side_0': frozenset(side_0),
|
||||
'side_1': frozenset(side_1),
|
||||
'merged': frozenset(merged),
|
||||
}
|
||||
E |= set(named.values())
|
||||
all_named.append(named)
|
||||
return H, coloring, all_named
|
||||
|
||||
|
||||
def is_all_distinct(edges, col, all_named):
|
||||
for named in all_named:
|
||||
i_s = edge_idx(edges, named['spike'])
|
||||
i_m = edge_idx(edges, named['merged'])
|
||||
if i_s is None or i_m is None:
|
||||
return False
|
||||
if col[i_s] == col[i_m]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
print("Reconstructing Conj 3.6 counterexample: n=14, tri#1, i_red=0")
|
||||
for G in graphs.triangulations(14, minimum_degree=5):
|
||||
D = dual_of(G)
|
||||
D.is_planar(set_embedding=True)
|
||||
chosen = None
|
||||
for face in D.faces():
|
||||
if len(face) != 5:
|
||||
continue
|
||||
res = apply_reduction(D, face, 0, 9999)
|
||||
if res is None:
|
||||
continue
|
||||
H_1, named_1 = res['H'], res['named']
|
||||
edges_1, colorings_1 = proper_3_edge_colorings(H_1)
|
||||
cand = [c for c in colorings_1
|
||||
if matches_chord_apex_kempe(edges_1, c, named_1)]
|
||||
if cand:
|
||||
chosen = (face, res, cand)
|
||||
break
|
||||
if chosen is None:
|
||||
continue
|
||||
face, res, cand = chosen
|
||||
H_1, named_1 = res['H'], res['named']
|
||||
edges_1, _ = proper_3_edge_colorings(H_1)
|
||||
if not cand:
|
||||
continue
|
||||
phi_1 = cand[0]
|
||||
phi_1_dict = {frozenset((e[0], e[1])): c for e, c in zip(edges_1, phi_1)}
|
||||
H_final, phi_final_dict, all_named = run_algorithm_to_completion(
|
||||
H_1, phi_1_dict, named_1)
|
||||
edges_f, colorings_f = proper_3_edge_colorings(H_final)
|
||||
print(f" H_t*: |V|={H_final.order()}, |E|={H_final.size()}, "
|
||||
f"{len(colorings_f)} proper 3-edge-colorings, "
|
||||
f"{len(all_named)} named-edge steps.")
|
||||
|
||||
# Kempe equivalence classes via union-find
|
||||
col_idx = {c: i for i, c in enumerate(colorings_f)}
|
||||
parent = list(range(len(colorings_f)))
|
||||
|
||||
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 c_idx, col in enumerate(colorings_f):
|
||||
for pair in [(0, 1), (0, 2), (1, 2)]:
|
||||
visited = set()
|
||||
for start in range(len(edges_f)):
|
||||
if col[start] not in pair or start in visited:
|
||||
continue
|
||||
cyc = kempe_cycle(edges_f, 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])
|
||||
|
||||
roots = {}
|
||||
for i in range(len(colorings_f)):
|
||||
r = find(i)
|
||||
roots.setdefault(r, []).append(i)
|
||||
|
||||
# phi_t* identification
|
||||
phi_final = tuple(phi_final_dict[frozenset((e[0], e[1]))]
|
||||
for e in edges_f)
|
||||
phi_idx = colorings_f.index(phi_final)
|
||||
phi_root = find(phi_idx)
|
||||
|
||||
print()
|
||||
print(f" Found {len(roots)} Kempe equivalence classes:")
|
||||
for r, members in sorted(roots.items(), key=lambda kv: -len(kv[1])):
|
||||
ad = [m for m in members
|
||||
if is_all_distinct(edges_f, colorings_f[m], all_named)]
|
||||
label = " (contains phi_t*)" if r == phi_root else ""
|
||||
print(f" class size {len(members)}: "
|
||||
f"{len(ad)} all-distinct colorings{label}")
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,432 @@
|
||||
"""Test Conjecture 3.8 (strengthening of 3.6 with the {b,c}-Kempe / 3-colour
|
||||
constraint on the new 4-edge face) on all min-degree-5 triangulations up to
|
||||
n = 18.
|
||||
|
||||
For each (G, F_v, i_red, varphi) with varphi a chord-apex + Kempe coloring,
|
||||
we look for a witness (F, e_1, e_2) of Conjecture 3.6 (clauses 1-3 including
|
||||
the 4-edge-face criterion). Then we:
|
||||
- subdivide e_1, e_2 by X_1, X_2,
|
||||
- add the new edge X_1 X_2,
|
||||
- recolour the (subdivided) Kempe cycle alternately starting from merged
|
||||
so propriety holds and the new edge takes the third colour,
|
||||
- identify f_n (the 4-edge face containing the new edge), and
|
||||
- test clause 4:
|
||||
EITHER partial(f_n) uses all 3 colours,
|
||||
OR the {b, c}-Kempe cycle through X_1 X_2 has only X_1 X_2 itself in
|
||||
partial(f_n).
|
||||
Aggregates per-n.
|
||||
|
||||
Run with: sage experiments/check_conj_3_8_scaled.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)
|
||||
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_set(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 trace_kempe_cycle(edges, col_list, start_idx, color_pair):
|
||||
cycle_set = kempe_cycle_set(edges, col_list, start_idx, color_pair)
|
||||
incident_at = {}
|
||||
for ei in cycle_set:
|
||||
u, v = edges[ei][0], edges[ei][1]
|
||||
incident_at.setdefault(u, []).append(ei)
|
||||
incident_at.setdefault(v, []).append(ei)
|
||||
start_u, start_v = edges[start_idx][0], edges[start_idx][1]
|
||||
walk = [(start_idx, start_v)]
|
||||
cur_e = start_idx
|
||||
cur_leave = start_v
|
||||
while True:
|
||||
nbrs = incident_at[cur_leave]
|
||||
if len(nbrs) != 2: break
|
||||
nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1]
|
||||
u2, v2 = edges[nxt][0], edges[nxt][1]
|
||||
leave_next = v2 if u2 == cur_leave else u2
|
||||
if nxt == start_idx: break
|
||||
walk.append((nxt, leave_next))
|
||||
cur_e = nxt
|
||||
cur_leave = leave_next
|
||||
return walk
|
||||
|
||||
|
||||
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_set(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_set(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 find_all_36_witnesses(H, edges, col_list, named):
|
||||
"""Yield every (F, e_1, e_2) satisfying clauses 1-3 of Conjecture 3.6."""
|
||||
merged_idx = edge_idx(edges, named['merged'])
|
||||
c_merged = col_list[merged_idx]
|
||||
kempe_cycles = []
|
||||
for c_prime in range(3):
|
||||
if c_prime == c_merged: continue
|
||||
kc = kempe_cycle_set(edges, col_list, merged_idx, (c_merged, c_prime))
|
||||
kempe_cycles.append((c_prime, kc))
|
||||
H.is_planar(set_embedding=True)
|
||||
out = []
|
||||
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_list[e1] != col_list[e2]: continue
|
||||
gap_a = (j - i - 1)
|
||||
gap_b = (n_face - 2 - gap_a)
|
||||
if gap_a != 1 and gap_b != 1: continue
|
||||
for c_prime, kc in kempe_cycles:
|
||||
if e1 in kc and e2 in kc:
|
||||
if gap_a == 1:
|
||||
e_F = face_edge_indices[i + 1]
|
||||
else:
|
||||
e_F = face_edge_indices[(j + 1) % n_face]
|
||||
out.append({
|
||||
'face_edges': face_edge_indices,
|
||||
'e1': e1, 'e2': e2, 'e_F': e_F,
|
||||
'kc_color_pair': (c_merged, c_prime),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def check_clause_4(H, edges, col_list, named, witness):
|
||||
"""Construct the modified H + recoloring, identify f_n, check clause 4."""
|
||||
e1, e2 = witness['e1'], witness['e2']
|
||||
e_F = witness['e_F']
|
||||
a = col_list[e1] # color of e_1 = e_2 (clauses 1)
|
||||
merged_idx = edge_idx(edges, named['merged'])
|
||||
cyc_a, cyc_b = witness['kc_color_pair'] # = (c_merged, c_other)
|
||||
# On the {cyc_a, cyc_b}-Kempe cycle, e_1 carries colour a, which
|
||||
# equals either cyc_a or cyc_b. The conjecture's "b" is the OTHER
|
||||
# colour on the cycle; we set it accordingly.
|
||||
if a == cyc_a:
|
||||
b = cyc_b
|
||||
elif a == cyc_b:
|
||||
b = cyc_a
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"e_1 colour {a} not in Kempe pair {(cyc_a, cyc_b)}")
|
||||
c = ({0, 1, 2} - {a, b}).pop()
|
||||
|
||||
# Subdivided cycle K' alternates blue/green starting from merged = blue
|
||||
walk = trace_kempe_cycle(edges, col_list, merged_idx, (cyc_a, cyc_b))
|
||||
walk_edges = [w[0] for w in walk]
|
||||
leave_at = [w[1] for w in walk]
|
||||
|
||||
# K' position counter. For each old cycle edge e in cyclic order, position
|
||||
# increments by 1 normally; for e1/e2, increments by 2 (two halves).
|
||||
# For e1, we record (entry_half_color, exit_half_color) where entry_half
|
||||
# is the half adjacent to entry_vertex and exit_half adjacent to leaving.
|
||||
e1_entry_color = e1_exit_color = None
|
||||
e2_entry_color = e2_exit_color = None
|
||||
e1_entry_vertex = e1_exit_vertex = None
|
||||
e2_entry_vertex = e2_exit_vertex = None
|
||||
other_new_colors = {} # ei -> new color (for cycle edges other than e1/e2)
|
||||
pos = 0
|
||||
for k, ei in enumerate(walk_edges):
|
||||
leaving = leave_at[k]
|
||||
u, v = edges[ei][0], edges[ei][1]
|
||||
entry = v if leaving == u else u
|
||||
if ei == e1:
|
||||
c_entry = cyc_b if pos % 2 == 0 else cyc_a
|
||||
pos += 1
|
||||
c_exit = cyc_b if pos % 2 == 0 else cyc_a
|
||||
pos += 1
|
||||
e1_entry_color = c_entry; e1_exit_color = c_exit
|
||||
e1_entry_vertex = entry; e1_exit_vertex = leaving
|
||||
elif ei == e2:
|
||||
c_entry = cyc_b if pos % 2 == 0 else cyc_a
|
||||
pos += 1
|
||||
c_exit = cyc_b if pos % 2 == 0 else cyc_a
|
||||
pos += 1
|
||||
e2_entry_color = c_entry; e2_exit_color = c_exit
|
||||
e2_entry_vertex = entry; e2_exit_vertex = leaving
|
||||
else:
|
||||
nc = cyc_b if pos % 2 == 0 else cyc_a
|
||||
other_new_colors[ei] = nc
|
||||
pos += 1
|
||||
|
||||
# Identify which half of e1 is on f_n: it's the half adjacent to e_F.
|
||||
# e_F has 2 endpoints; one is shared with e1, one with e2.
|
||||
e_F_endpoints = set(edges[e_F])
|
||||
e1_endpoints = set(edges[e1])
|
||||
e2_endpoints = set(edges[e2])
|
||||
shared_e1_eF = (e_F_endpoints & e1_endpoints).pop()
|
||||
shared_e2_eF = (e_F_endpoints & e2_endpoints).pop()
|
||||
# The half of e1 on f_n connects X_1 to shared_e1_eF. So if shared_e1_eF
|
||||
# equals e1_entry_vertex, the half on f_n is the "entry half"; else the
|
||||
# "exit half".
|
||||
if shared_e1_eF == e1_entry_vertex:
|
||||
e1_h_color = e1_entry_color
|
||||
else:
|
||||
e1_h_color = e1_exit_color
|
||||
if shared_e2_eF == e2_entry_vertex:
|
||||
e2_h_color = e2_entry_color
|
||||
else:
|
||||
e2_h_color = e2_exit_color
|
||||
|
||||
# Determine e_F's new colour: if e_F is on the Kempe cycle (other_new_colors)
|
||||
# use that, else original.
|
||||
if e_F in other_new_colors:
|
||||
e_F_color = other_new_colors[e_F]
|
||||
else:
|
||||
e_F_color = col_list[e_F]
|
||||
|
||||
# f_n's 4 edge colors: [new edge X_1-X_2 = c, e1_h, e_F, e2_h]
|
||||
fn_colors = [c, e1_h_color, e_F_color, e2_h_color]
|
||||
fn_distinct = set(fn_colors)
|
||||
uses_3_colors = (len(fn_distinct) == 3)
|
||||
|
||||
if uses_3_colors:
|
||||
return True # clause 4(i) satisfied
|
||||
|
||||
# Otherwise, check clause 4(ii): the {b,c}-Kempe cycle through the new
|
||||
# edge has only X_1-X_2 in f_n's boundary.
|
||||
# We need to actually build the modified graph and trace this cycle.
|
||||
return check_clause_4_kempe_part(H, edges, col_list, named, witness,
|
||||
a, b, c, e1_h_color, e2_h_color,
|
||||
other_new_colors,
|
||||
e1_entry_vertex, e1_exit_vertex,
|
||||
e2_entry_vertex, e2_exit_vertex,
|
||||
e1_entry_color, e1_exit_color,
|
||||
e2_entry_color, e2_exit_color)
|
||||
|
||||
|
||||
def check_clause_4_kempe_part(H, edges, col_list, named, witness, a, b, c,
|
||||
e1_h_color, e2_h_color, other_new_colors,
|
||||
e1_ev, e1_xv, e2_ev, e2_xv,
|
||||
e1_ec, e1_xc, e2_ec, e2_xc):
|
||||
"""Build modified H' and check whether the {b,c}-Kempe cycle through X1-X2
|
||||
has only that one edge in partial(f_n)."""
|
||||
e1 = witness['e1']; e2 = witness['e2']; e_F = witness['e_F']
|
||||
H2 = H.copy()
|
||||
X1 = max(v for v in H.vertices(sort=False) if isinstance(v, int)) + 1
|
||||
X2 = X1 + 1
|
||||
H2.add_vertex(X1); H2.add_vertex(X2)
|
||||
e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2])
|
||||
H2.delete_edge(e1_uv); H2.delete_edge(e2_uv)
|
||||
H2.add_edges([(e1_uv[0], X1), (X1, e1_uv[1]),
|
||||
(e2_uv[0], X2), (X2, e2_uv[1]),
|
||||
(X1, X2)])
|
||||
# Build coloring for H2
|
||||
new_coloring = {}
|
||||
# Copy non-modified edges: original color (unless in other_new_colors)
|
||||
for ei, c0 in enumerate(col_list):
|
||||
if ei == e1 or ei == e2: continue
|
||||
e_fs = frozenset(edges[ei])
|
||||
if ei in other_new_colors:
|
||||
new_coloring[e_fs] = other_new_colors[ei]
|
||||
else:
|
||||
new_coloring[e_fs] = c0
|
||||
# Halves of e1, e2
|
||||
new_coloring[frozenset((e1_ev, X1))] = e1_ec
|
||||
new_coloring[frozenset((X1, e1_xv))] = e1_xc
|
||||
new_coloring[frozenset((e2_ev, X2))] = e2_ec
|
||||
new_coloring[frozenset((X2, e2_xv))] = e2_xc
|
||||
new_coloring[frozenset((X1, X2))] = c
|
||||
|
||||
# Determine f_n's edges (in H2): new edge X1-X2, the half of e1 adjacent
|
||||
# to e_F's shared vertex with e1, e_F itself, and the half of e2 adjacent
|
||||
# to e_F's shared vertex with e2.
|
||||
e_F_endpoints = set(edges[e_F])
|
||||
e1_endpoints = set(edges[e1])
|
||||
e2_endpoints = set(edges[e2])
|
||||
shared_e1_eF = (e_F_endpoints & e1_endpoints).pop()
|
||||
shared_e2_eF = (e_F_endpoints & e2_endpoints).pop()
|
||||
fn_edges = {
|
||||
frozenset((X1, X2)),
|
||||
frozenset((X1, shared_e1_eF)),
|
||||
frozenset(edges[e_F]),
|
||||
frozenset((X2, shared_e2_eF)),
|
||||
}
|
||||
|
||||
# Build edge list of H2 + coloring list
|
||||
H2_edges = list(H2.edges(labels=False))
|
||||
H2_col_list = [new_coloring[frozenset(e)] for e in H2_edges]
|
||||
# Sanity: verify phi' is a proper 3-edge-colouring of H2 (the conjecture
|
||||
# asserts this; if the construction is wrong, abort).
|
||||
for v in H2.vertex_iterator():
|
||||
seen = []
|
||||
for w in H2.neighbor_iterator(v):
|
||||
seen.append(new_coloring[frozenset((v, w))])
|
||||
if len(set(seen)) != len(seen):
|
||||
raise RuntimeError(
|
||||
f"phi' is not proper at vertex {v}: colors {seen}")
|
||||
# Trace the {b,c}-Kempe cycle through X1-X2 in H2 using new_coloring
|
||||
H2_X1X2_idx = None
|
||||
for i, e in enumerate(H2_edges):
|
||||
if frozenset(e) == frozenset((X1, X2)):
|
||||
H2_X1X2_idx = i; break
|
||||
if H2_X1X2_idx is None:
|
||||
return False
|
||||
kc_bc = kempe_cycle_set(H2_edges, H2_col_list, H2_X1X2_idx, (b, c))
|
||||
# Count edges in kc_bc that are in fn_edges
|
||||
count = 0
|
||||
for ei in kc_bc:
|
||||
if frozenset(H2_edges[ei]) in fn_edges:
|
||||
count += 1
|
||||
return count == 1
|
||||
|
||||
|
||||
def main(max_n=20, time_budget_per_n=7200):
|
||||
rows = []
|
||||
for n in range(12, max_n + 1):
|
||||
start = time.time()
|
||||
try:
|
||||
triangulations = list(graphs.triangulations(n, minimum_degree=5))
|
||||
except Exception as ex:
|
||||
rows.append((n, 0, 0, 0, f"cannot enumerate"))
|
||||
continue
|
||||
n_tri = len(triangulations)
|
||||
total_col = 0
|
||||
total_pass = 0
|
||||
timed_out = False
|
||||
for tri_idx, G in enumerate(triangulations, start=1):
|
||||
if time.time() - start > time_budget_per_n:
|
||||
timed_out = True; break
|
||||
G.is_planar(set_embedding=True)
|
||||
D = dual_of(G); D.is_planar(set_embedding=True)
|
||||
for face in D.faces():
|
||||
if len(face) != 5: continue
|
||||
if time.time() - start > time_budget_per_n:
|
||||
timed_out = True; break
|
||||
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)]
|
||||
for col in cand:
|
||||
witnesses = find_all_36_witnesses(H, edges, list(col),
|
||||
named)
|
||||
if not witnesses:
|
||||
continue
|
||||
total_col += 1
|
||||
ok = False
|
||||
for w in witnesses:
|
||||
try:
|
||||
if check_clause_4(H, edges, list(col), named,
|
||||
w):
|
||||
ok = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if ok:
|
||||
total_pass += 1
|
||||
elapsed = time.time() - start
|
||||
status = (f"TIMEOUT ({elapsed:.0f}s)" if timed_out
|
||||
else f"complete ({elapsed:.0f}s)")
|
||||
rows.append((n, n_tri, total_col, total_pass, status))
|
||||
print(f"n={n}: {n_tri} tri, {total_col} cand witnesses, "
|
||||
f"{total_pass} pass clause 4, {status}")
|
||||
sys.stdout.flush()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(f"{'n':>3} {'#tri':>5} {'#witness':>10} {'#pass_cl4':>10} {'status':>25}")
|
||||
print("-" * 70)
|
||||
for n, n_tri, n_col, n_pass, status in rows:
|
||||
print(f"{n:>3} {n_tri:>5} {n_col:>10} {n_pass:>10} {status:>25}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,200 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Run conjecture 3.6 test specifically on n=15 with progress output."""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
import sys
|
||||
|
||||
# Reuse helpers from check_conj_face_kempe.py by importing
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/face_monochromatic_pairs/experiments')
|
||||
from check_conj_face_kempe import (
|
||||
dual_of, apply_reduction, proper_3_edge_colorings,
|
||||
kempe_cycle, edge_idx, matches_chord_apex_kempe,
|
||||
conjecture_holds_for
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
n = 15
|
||||
print(f"=== n = {n} ===")
|
||||
triangulations = list(graphs.triangulations(n, minimum_degree=5))
|
||||
print(f" Found {len(triangulations)} triangulations.")
|
||||
total_cand = 0
|
||||
total_pass = 0
|
||||
total_fail = 0
|
||||
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)
|
||||
pentagonal_faces = [f for f in D.faces() if len(f) == 5]
|
||||
for face_no, face in enumerate(pentagonal_faces):
|
||||
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
|
||||
total_cand += len(cand)
|
||||
total_pass += n_pass
|
||||
total_fail += n_fail
|
||||
status = "PASSED" if n_fail == 0 else "*** FAILED ***"
|
||||
print(f" tri#{tri_idx} face#{face_no} i_red={i_red}: "
|
||||
f"{len(cand)} chord-apex+Kempe; {n_pass} pass, "
|
||||
f"{n_fail} fail. {status}")
|
||||
sys.stdout.flush()
|
||||
if tri_idx % 5 == 0:
|
||||
print(f" ... finished tri#{tri_idx}/{len(triangulations)}; "
|
||||
f"cumulative cand={total_cand}, pass={total_pass}, "
|
||||
f"fail={total_fail}")
|
||||
sys.stdout.flush()
|
||||
print(f"\nTotal: {total_cand} chord-apex+Kempe colorings tested; "
|
||||
f"{total_pass} pass, {total_fail} fail.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Test Conjecture 3.6 (with the 4-edge-face criterion) across all
|
||||
min-degree-5 triangulations up to n = 18. Aggregates per-n totals.
|
||||
|
||||
Run with: sage experiments/check_conj_face_kempe_scaled.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)
|
||||
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):
|
||||
"""Full Conjecture 3.6 check: face F with two same-colour edges e1, e2
|
||||
(neither equal to merged), both on a common Kempe cycle through merged,
|
||||
and exactly one edge of partial F lies between them in one arc."""
|
||||
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
|
||||
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 True
|
||||
return False
|
||||
|
||||
|
||||
def main(max_n=22, time_budget_per_n=1800):
|
||||
rows = []
|
||||
for n in range(12, max_n + 1):
|
||||
start = time.time()
|
||||
try:
|
||||
triangulations = list(graphs.triangulations(n, minimum_degree=5))
|
||||
except Exception as ex:
|
||||
print(f"n={n}: cannot enumerate: {ex}")
|
||||
rows.append((n, None, None, None, 'cannot enumerate'))
|
||||
continue
|
||||
n_tri = len(triangulations)
|
||||
total_col = 0
|
||||
total_pass = 0
|
||||
timed_out = False
|
||||
for tri_idx, G in enumerate(triangulations, start=1):
|
||||
if time.time() - start > time_budget_per_n:
|
||||
timed_out = True
|
||||
break
|
||||
G.is_planar(set_embedding=True)
|
||||
D = dual_of(G); D.is_planar(set_embedding=True)
|
||||
for face in D.faces():
|
||||
if len(face) != 5: continue
|
||||
if time.time() - start > time_budget_per_n:
|
||||
timed_out = True
|
||||
break
|
||||
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)]
|
||||
for col in cand:
|
||||
total_col += 1
|
||||
if conjecture_holds_for(H, edges, col, named):
|
||||
total_pass += 1
|
||||
elapsed = time.time() - start
|
||||
status = (f"TIMEOUT after {elapsed:.0f}s" if timed_out
|
||||
else f"complete ({elapsed:.0f}s)")
|
||||
rows.append((n, n_tri, total_col, total_pass, status))
|
||||
print(f"n={n}: {n_tri} tri, {total_col} colorings, "
|
||||
f"{total_pass} pass, {status}")
|
||||
sys.stdout.flush()
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f"{'n':>3} {'#tri':>5} {'#col':>10} {'#pass':>10} {'status':>20}")
|
||||
print("-" * 60)
|
||||
for n, n_tri, n_col, n_pass, status in rows:
|
||||
n_tri_s = str(n_tri) if n_tri is not None else '-'
|
||||
n_col_s = str(n_col) if n_col is not None else '-'
|
||||
n_pass_s = str(n_pass) if n_pass is not None else '-'
|
||||
print(f"{n:>3} {n_tri_s:>5} {n_col_s:>10} {n_pass_s:>10} {status:>20}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Test the face-monochromatic-pair conjecture (both the weak form,
|
||||
clauses 1-3, and the strengthening, clauses 1-4) on the 6 duals of the
|
||||
non-Hamiltonian 38-vertex cubic plane graphs found by Holton & McKay.
|
||||
|
||||
Each Holton-McKay graph is itself a cubic plane graph $G'$; its dual is a
|
||||
21-vertex triangulation $G$. We treat each Holton-McKay graph as the $G'$
|
||||
of a hypothetical minimal counterexample and apply the reduced-dual
|
||||
construction at each of its pentagonal faces.
|
||||
|
||||
Reuses the testing infrastructure from check_conj_3_8_scaled.py.
|
||||
|
||||
Run with: sage experiments/check_conj_on_holton_mckay.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sage.all import Graph
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
from check_conj_3_8_scaled import (
|
||||
apply_reduction,
|
||||
proper_3_edge_colorings,
|
||||
matches_chord_apex_kempe,
|
||||
find_all_36_witnesses,
|
||||
check_clause_4,
|
||||
)
|
||||
from check_conj_face_kempe_scaled import conjecture_holds_for
|
||||
|
||||
|
||||
HM_FILE = ('/Users/didericis/Code/math-research/papers/'
|
||||
'even_level_graph_generators/experiments/nonham38m4.pc')
|
||||
|
||||
|
||||
def parse_planar_code_to_sage(path):
|
||||
"""Parse McKay's planar_code file and return list of Sage Graphs (each
|
||||
a 3-connected cubic plane graph)."""
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
header = b'>>planar_code<<'
|
||||
assert data.startswith(header), data[:20]
|
||||
pos = len(header)
|
||||
sage_graphs = []
|
||||
while pos < len(data):
|
||||
n = data[pos]; pos += 1
|
||||
edges = set()
|
||||
for v in range(n):
|
||||
while True:
|
||||
w = data[pos]; pos += 1
|
||||
if w == 0:
|
||||
break
|
||||
edges.add(frozenset((v, w - 1)))
|
||||
G = Graph([tuple(e) for e in edges], multiedges=False, loops=False)
|
||||
G.is_planar(set_embedding=True)
|
||||
sage_graphs.append(G)
|
||||
return sage_graphs
|
||||
|
||||
|
||||
def test_one(D, name=""):
|
||||
"""Test conjecture on one cubic plane graph $D$ ($= G'$). Returns
|
||||
(n_cand, n_sat_123, n_sat_1234, faces_used, indices_used)."""
|
||||
D.is_planar(set_embedding=True)
|
||||
n_cand = n_sat_123 = n_sat_1234 = 0
|
||||
face_count = 0
|
||||
for face in D.faces():
|
||||
if len(face) != 5:
|
||||
continue
|
||||
face_count += 1
|
||||
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)]
|
||||
for col in cand:
|
||||
n_cand += 1
|
||||
if conjecture_holds_for(H, edges, col, named):
|
||||
n_sat_123 += 1
|
||||
# clauses 1-4: existential at the witness level
|
||||
witnesses = find_all_36_witnesses(H, edges, col, named)
|
||||
if any(check_clause_4(H, edges, col, named, w)
|
||||
for w in witnesses):
|
||||
n_sat_1234 += 1
|
||||
return n_cand, n_sat_123, n_sat_1234, face_count
|
||||
|
||||
|
||||
def main():
|
||||
graphs_list = parse_planar_code_to_sage(HM_FILE)
|
||||
print(f"Loaded {len(graphs_list)} Holton-McKay graphs from "
|
||||
f"{HM_FILE}\n")
|
||||
|
||||
grand_cand = grand_123 = grand_1234 = 0
|
||||
rows = []
|
||||
import time
|
||||
for i, G in enumerate(graphs_list):
|
||||
n = G.order()
|
||||
m = G.size()
|
||||
degs = sorted(set(G.degree()))
|
||||
t0 = time.time()
|
||||
n_cand, n_123, n_1234, n_pent = test_one(G, name=f"HM{i}")
|
||||
dt = time.time() - t0
|
||||
rows.append((i, n, m, degs, n_pent, n_cand, n_123, n_1234, dt))
|
||||
grand_cand += n_cand
|
||||
grand_123 += n_123
|
||||
grand_1234 += n_1234
|
||||
sys_stdout = sys.stdout
|
||||
print(f"HM{i}: n={n} m={m} deg={degs} pent_faces={n_pent} "
|
||||
f"cand={n_cand} sat(1-3)={n_123} sat(1-4)={n_1234} "
|
||||
f"[{dt:.1f}s]")
|
||||
sys.stdout.flush()
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print(f"{'i':>2} {'n':>3} {'m':>4} {'#pent':>6} {'#cand':>8} "
|
||||
f"{'#sat(1-3)':>10} {'#sat(1-4)':>10} {'time':>7}")
|
||||
print("-" * 78)
|
||||
for i, n, m, degs, n_pent, n_cand, n_123, n_1234, dt in rows:
|
||||
print(f"{i:>2} {n:>3} {m:>4} {n_pent:>6} {n_cand:>8} "
|
||||
f"{n_123:>10} {n_1234:>10} {dt:>6.1f}s")
|
||||
print("-" * 78)
|
||||
print(f"{'tot':>2} {'-':>3} {'-':>4} {'-':>6} {grand_cand:>8} "
|
||||
f"{grand_123:>10} {grand_1234:>10}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Constrained 3-edge-colouring feasibility on H_t*.
|
||||
|
||||
The chord-apex (Lemma 2.6) and Kempe-cycle (Lemma 2.7) lemmas give proven
|
||||
constraints on proper 3-edge-colourings of H_1 when G is a minimal
|
||||
counterexample. The step-1 constraints, *interpreted on H_t*, are:
|
||||
|
||||
(C1) phi(spike_1) = phi(merged_1) [chord-apex]
|
||||
(K0) the {phi(spike_1), phi(side_0_1)}-Kempe cycle of H_t* through
|
||||
spike_1 contains side_0_1 and merged_1
|
||||
(K1) the {phi(spike_1), phi(side_1_1)}-Kempe cycle of H_t* through
|
||||
spike_1 contains side_1_1 and merged_1
|
||||
|
||||
If G is a minimal counterexample then every proper 3-edge-colouring of H_t*
|
||||
that *lifts back* to a proper 3-edge-colouring of H_1 must satisfy (C1) +
|
||||
(K0) + (K1) on H_1; the Kempe-cycle structure on H_t* and H_1 can differ,
|
||||
but we test the H_t*-interpreted versions here as the natural constraint
|
||||
set the algorithm propagates.
|
||||
|
||||
For each min-deg-5 triangulation G and each (face, i_red) we enumerate
|
||||
proper 3-edge-colourings of H_t* and count those satisfying these step-1
|
||||
constraints. If the constrained problem is INFEASIBLE on H_t* for some G
|
||||
in our test set, that would be a hint that constraints alone can rule out
|
||||
3-edge-colourability of H_t* -- a potential mechanism for proving smaller
|
||||
counterexamples exist.
|
||||
|
||||
Run with: sage experiments/check_constrained_feasibility.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
from sage.graphs.graph_coloring import edge_coloring
|
||||
import sys
|
||||
|
||||
|
||||
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 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 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 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_to_completion(H, phi_dict, named_step1, vn_start=10000):
|
||||
H = H.copy()
|
||||
coloring = dict(phi_dict)
|
||||
all_named = [named_step1]
|
||||
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)
|
||||
s0 = (v_n, A[i_t]); sp = (v_n, A[(i_t+1) % 5])
|
||||
s1 = (v_n, A[(i_t+2) % 5]); mg = (A[(i_t+3) % 5], A[(i_t+4) % 5])
|
||||
H_new.add_edges([s0, sp, s1, mg])
|
||||
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(s0)] = f_vec[i_t]
|
||||
coloring[frozenset(sp)] = f_vec[(i_t+1) % 5]
|
||||
coloring[frozenset(s1)] = f_vec[(i_t+2) % 5]
|
||||
coloring[frozenset(mg)] = f_vec[(i_t+3) % 5]
|
||||
all_named.append({
|
||||
'spike': frozenset(sp), 'side_0': frozenset(s0),
|
||||
'side_1': frozenset(s1), 'merged': frozenset(mg),
|
||||
})
|
||||
E |= set(all_named[-1].values())
|
||||
return H, coloring, all_named
|
||||
|
||||
|
||||
def check_constraints(edges, col, named):
|
||||
"""Check (C1) + (K0) + (K1) on this colouring."""
|
||||
idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
|
||||
if any(v is None for v in idx.values()):
|
||||
return False, False, False
|
||||
c_spike = col[idx['spike']]
|
||||
c_merged = col[idx['merged']]
|
||||
C1 = (c_spike == c_merged)
|
||||
if not C1:
|
||||
return False, False, False
|
||||
c_s0 = col[idx['side_0']]; c_s1 = col[idx['side_1']]
|
||||
kc0 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s0))
|
||||
kc1 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s1))
|
||||
K0 = idx['side_0'] in kc0 and idx['merged'] in kc0
|
||||
K1 = idx['side_1'] in kc1 and idx['merged'] in kc1
|
||||
return C1, K0, K1
|
||||
|
||||
|
||||
def run_case(G, n, tri_idx):
|
||||
G.is_planar(set_embedding=True)
|
||||
D = dual_of(G); D.is_planar(set_embedding=True)
|
||||
for face_n, 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_1, named_1 = res['H'], res['named']
|
||||
# Default phi_1: Sage's edge_coloring
|
||||
try:
|
||||
classes = edge_coloring(H_1, value_only=False)
|
||||
except Exception:
|
||||
continue
|
||||
if len(classes) > 3: continue
|
||||
phi_1_dict = {}
|
||||
for k, edge_list in enumerate(classes):
|
||||
for u, v in edge_list:
|
||||
phi_1_dict[frozenset([u, v])] = k
|
||||
H_final, _, all_named = run_to_completion(H_1, phi_1_dict, named_1)
|
||||
edges_f, colorings_f = proper_3_edge_colorings(H_final)
|
||||
n_total = len(colorings_f)
|
||||
n_c1 = 0; n_c1_k0 = 0; n_c1_k1 = 0; n_all = 0
|
||||
for col in colorings_f:
|
||||
C1, K0, K1 = check_constraints(edges_f, col, named_1)
|
||||
if C1: n_c1 += 1
|
||||
if C1 and K0: n_c1_k0 += 1
|
||||
if C1 and K1: n_c1_k1 += 1
|
||||
if C1 and K0 and K1: n_all += 1
|
||||
feasible = n_all > 0
|
||||
print(f" n={n} tri#{tri_idx} face#{face_n} i_red={i_red}: "
|
||||
f"|V(H_t*)|={H_final.order()}, |E|={H_final.size()}, "
|
||||
f"colorings={n_total}, "
|
||||
f"C1={n_c1}, C1+K0={n_c1_k0}, C1+K1={n_c1_k1}, "
|
||||
f"C1+K0+K1={n_all} "
|
||||
f"{'OK' if feasible else '*** INFEASIBLE ***'}")
|
||||
sys.stdout.flush()
|
||||
return # one (face, i_red) per triangulation
|
||||
|
||||
|
||||
def main(max_n=15):
|
||||
for n in range(12, max_n + 1):
|
||||
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):
|
||||
run_case(G, n, tri_idx)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Check whether the dodecahedron's reduced dual admits a proper
|
||||
3-edge-colouring with:
|
||||
(i) colour(spike) == colour(merged), AND
|
||||
(ii) the {c_spike, c_side0}-Kempe cycle through the spike contains both
|
||||
side-0 and the merged edge, AND
|
||||
(iii) the {c_spike, c_side1}-Kempe cycle through the spike contains both
|
||||
side-1 and the merged edge.
|
||||
|
||||
Definitions follow Algorithm 3.1 in paper.tex with G = icosahedron, G' =
|
||||
dodecahedron, i_1 = 0; the reduced dual has 16 vertices and 24 edges.
|
||||
|
||||
Lemmas 2.6 and 2.7 force these properties for ANY proper 3-edge-colouring
|
||||
of the reduced dual of a *minimal counterexample's* dual. The dodecahedron
|
||||
is the dual of the icosahedron --- which IS 4-colourable, so the lemmas do
|
||||
not apply and the question is non-trivial.
|
||||
"""
|
||||
from sage.all import Graph
|
||||
|
||||
|
||||
def build_reduced_dodecahedron(i_red=0):
|
||||
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)))
|
||||
v_n = 'v_n'
|
||||
edges.append((v_n, ('b', i_red % 5)))
|
||||
edges.append((v_n, ('b', (i_red + 1) % 5)))
|
||||
edges.append((v_n, ('b', (i_red + 2) % 5)))
|
||||
edges.append((('b', (i_red + 3) % 5), ('b', (i_red + 4) % 5)))
|
||||
return Graph(edges, multiedges=False, loops=False)
|
||||
|
||||
|
||||
def enumerate_proper_3_edge_colorings(G):
|
||||
"""Return (edge_list, list_of_colorings) where each coloring is a list of
|
||||
3-edge-colours indexed by edge_list."""
|
||||
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(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(edges, coloring, start_edge_idx, color_pair):
|
||||
"""Return the set of edge-indices in the Kempe cycle containing
|
||||
edges[start_edge_idx] in the subgraph of edges with colour in
|
||||
color_pair."""
|
||||
a, b = color_pair
|
||||
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
|
||||
if start_edge_idx not in in_sub:
|
||||
return None
|
||||
visited = {start_edge_idx}
|
||||
stack = [start_edge_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 main():
|
||||
G = build_reduced_dodecahedron(i_red=0)
|
||||
print(f"|V| = {G.order()}, |E| = {G.size()}")
|
||||
|
||||
edges, colorings = enumerate_proper_3_edge_colorings(G)
|
||||
print(f"Proper 3-edge-colorings total: {len(colorings)}")
|
||||
|
||||
v_n = 'v_n'
|
||||
spike_set = frozenset({v_n, ('b', 1)})
|
||||
side_0_set = frozenset({v_n, ('b', 0)})
|
||||
side_1_set = frozenset({v_n, ('b', 2)})
|
||||
merged_set = frozenset({('b', 3), ('b', 4)})
|
||||
|
||||
idx = {}
|
||||
for i, e in enumerate(edges):
|
||||
s = frozenset((e[0], e[1]))
|
||||
if s == spike_set: idx['spike'] = i
|
||||
if s == side_0_set: idx['side_0'] = i
|
||||
if s == side_1_set: idx['side_1'] = i
|
||||
if s == merged_set: idx['merged'] = i
|
||||
|
||||
n_chord_apex = 0 # spike == merged
|
||||
n_kc0_ok = 0 # condition (ii)
|
||||
n_kc1_ok = 0 # condition (iii)
|
||||
n_all_ok = 0 # all three
|
||||
example = None
|
||||
|
||||
for col in colorings:
|
||||
c_spike = col[idx['spike']]
|
||||
c_merged = col[idx['merged']]
|
||||
c_s0 = col[idx['side_0']]
|
||||
c_s1 = col[idx['side_1']]
|
||||
if c_spike != c_merged:
|
||||
continue
|
||||
n_chord_apex += 1
|
||||
|
||||
kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
|
||||
kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
|
||||
|
||||
kc0_ok = kc0 is not None and idx['side_0'] in kc0 and idx['merged'] in kc0
|
||||
kc1_ok = kc1 is not None and idx['side_1'] in kc1 and idx['merged'] in kc1
|
||||
n_kc0_ok += int(kc0_ok)
|
||||
n_kc1_ok += int(kc1_ok)
|
||||
if kc0_ok and kc1_ok:
|
||||
n_all_ok += 1
|
||||
if example is None:
|
||||
example = (col, kc0, kc1)
|
||||
|
||||
print()
|
||||
print(f"Colorings with spike == merged (chord-apex condition): {n_chord_apex}")
|
||||
print(f" ... + {{c, c_0}}-Kempe cycle through spike/side-0/merged: {n_kc0_ok}")
|
||||
print(f" ... + {{c, c_1}}-Kempe cycle through spike/side-1/merged: {n_kc1_ok}")
|
||||
print(f" ... ALL THREE conditions: {n_all_ok}")
|
||||
|
||||
if example is not None:
|
||||
col, kc0, kc1 = example
|
||||
c_spike = col[idx['spike']]
|
||||
print()
|
||||
print("Example coloring (one of {} matching):".format(n_all_ok))
|
||||
for role in ('spike', 'side_0', 'side_1', 'merged'):
|
||||
print(f" {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
|
||||
print(f" spike colour = merged colour = {c_spike}")
|
||||
print(f" Kempe cycle {{c, c_0}} (through spike): {len(kc0)} edges")
|
||||
for i in sorted(kc0):
|
||||
print(f" [c={col[i]}] {edges[i]}")
|
||||
print(f" Kempe cycle {{c, c_1}} (through spike): {len(kc1)} edges")
|
||||
for i in sorted(kc1):
|
||||
print(f" [c={col[i]}] {edges[i]}")
|
||||
else:
|
||||
# Dissect a sample chord-apex coloring that fails the Kempe condition
|
||||
print()
|
||||
print("Sample chord-apex coloring (fails the Kempe-chain condition):")
|
||||
for col in colorings:
|
||||
if col[idx['spike']] != col[idx['merged']]:
|
||||
continue
|
||||
c_spike = col[idx['spike']]
|
||||
c_s0 = col[idx['side_0']]
|
||||
c_s1 = col[idx['side_1']]
|
||||
for role in ('spike', 'side_0', 'side_1', 'merged'):
|
||||
print(f" {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
|
||||
kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
|
||||
kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
|
||||
kc_m0 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s0))
|
||||
kc_m1 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s1))
|
||||
print(f" spike's {{c, c_0}}-Kempe cycle has {len(kc0)} edges; "
|
||||
f"side_0 in it = {idx['side_0'] in kc0}, "
|
||||
f"merged in it = {idx['merged'] in kc0}")
|
||||
print(f" merged's {{c, c_0}}-Kempe cycle has {len(kc_m0)} edges; "
|
||||
f"disjoint from spike's? {kc0.isdisjoint(kc_m0)}")
|
||||
print(f" spike's {{c, c_1}}-Kempe cycle has {len(kc1)} edges; "
|
||||
f"side_1 in it = {idx['side_1'] in kc1}, "
|
||||
f"merged in it = {idx['merged'] in kc1}")
|
||||
print(f" merged's {{c, c_1}}-Kempe cycle has {len(kc_m1)} edges; "
|
||||
f"disjoint from spike's? {kc1.isdisjoint(kc_m1)}")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,288 @@
|
||||
"""Investigate the conjecture: H_t* has the same number of Kempe equivalence
|
||||
classes as H_1.
|
||||
|
||||
For each min-deg-5 triangulation G (up to some n) and each reduction index
|
||||
i_red in {0,...,4}:
|
||||
- Build H_1 from the first pentagonal face of G' = dual(G).
|
||||
- Count Kempe classes of H_1.
|
||||
- Run Algorithm 3.1 to completion with Sage's edge_coloring as phi_1.
|
||||
- Count Kempe classes of H_t*.
|
||||
- Print a row.
|
||||
|
||||
Run with: sage experiments/check_kempe_class_invariance.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
from sage.graphs.graph_coloring import edge_coloring
|
||||
import sys
|
||||
|
||||
|
||||
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):
|
||||
"""Count the Kempe equivalence classes of proper 3-edge-colorings of G."""
|
||||
edges, colorings = proper_3_edge_colorings(G)
|
||||
if not colorings:
|
||||
return None, 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_coloring_dict, named_step1,
|
||||
vn_start=10000):
|
||||
H = H.copy()
|
||||
coloring = dict(phi_1_coloring_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, coloring
|
||||
|
||||
|
||||
def main():
|
||||
print(f"{'n':>3} {'tri#':>5} {'i_red':>5} | "
|
||||
f"{'|V(H_1)|':>9} {'|E(H_1)|':>9} {'#col(H_1)':>10} {'#K(H_1)':>8} | "
|
||||
f"{'|V(H_t*)|':>10} {'|E(H_t*)|':>10} {'#col(H_t*)':>11} {'#K(H_t*)':>10}")
|
||||
print("-" * 120)
|
||||
for n in [12, 14]:
|
||||
try:
|
||||
triangulations = list(graphs.triangulations(n, minimum_degree=5))
|
||||
except Exception as ex:
|
||||
print(f" cannot enumerate n={n}: {ex}")
|
||||
continue
|
||||
for tri_idx, G in enumerate(triangulations, start=1):
|
||||
G_copy = G.copy()
|
||||
G_copy.is_planar(set_embedding=True)
|
||||
D = dual_of(G_copy)
|
||||
D.is_planar(set_embedding=True)
|
||||
# iterate over all pentagonal faces (or just the first)
|
||||
faces = [f for f in D.faces() if len(f) == 5]
|
||||
if not faces:
|
||||
continue
|
||||
face = faces[0]
|
||||
for i_red in range(5):
|
||||
res = apply_reduction(D, face, i_red, 9999)
|
||||
if res is None:
|
||||
continue
|
||||
H_1 = res['H']
|
||||
# count Kempe classes of H_1
|
||||
n_col_h1, n_kc_h1 = num_kempe_classes(H_1)
|
||||
# run algorithm to completion with Sage's edge_coloring as phi_1
|
||||
classes = edge_coloring(H_1, value_only=False)
|
||||
if len(classes) > 3:
|
||||
continue
|
||||
phi_1_dict = {}
|
||||
for k, edge_list in enumerate(classes):
|
||||
for u, v in edge_list:
|
||||
phi_1_dict[frozenset([u, v])] = k
|
||||
H_final, _ = run_algorithm_to_completion(H_1, phi_1_dict,
|
||||
res['named'])
|
||||
n_col_hf, n_kc_hf = num_kempe_classes(H_final)
|
||||
same = "same" if n_kc_h1 == n_kc_hf else "DIFFERENT"
|
||||
print(f"{n:>3} {tri_idx:>5} {i_red:>5} | "
|
||||
f"{H_1.order():>9} {H_1.size():>9} {n_col_h1:>10} {n_kc_h1:>8} | "
|
||||
f"{H_final.order():>10} {H_final.size():>10} "
|
||||
f"{n_col_hf:>11} {n_kc_hf:>10} {same}")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,310 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,180 @@
|
||||
r"""Draw diagrams illustrating the proof of Lemma 2.6 (chord-apex).
|
||||
|
||||
Three figures rendered on the dodecahedral G':
|
||||
step1: the assumed proper 3-edge-colouring of \widehat{G}'_{v,0}, with the
|
||||
v_n-edges receiving distinct colours X (red), Y (green, spike),
|
||||
Z (blue), and the chord coloured W with W != Y, forcing W in {X, Z}.
|
||||
step2: lift to G' in the case W = Z. The externals inherit
|
||||
psi(f) = (X, Y, Z, Z, Z) and Lemma 2.4 completes partial F_v.
|
||||
step3: lift to G' in the case W = X with psi(f) = (X, Y, Z, X, X).
|
||||
"""
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.lines import Line2D
|
||||
|
||||
from reduced_dual import build_dual, apply_reduction
|
||||
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
C1 = '#dc2626' # red -> X
|
||||
C2 = '#16a34a' # green -> Y (spike)
|
||||
C3 = '#2563eb' # blue -> Z
|
||||
DARK = '#374151'
|
||||
DEG2 = '#f59e0b'
|
||||
GRAY = '#9ca3af'
|
||||
LIGHT = '#e5e7eb'
|
||||
APEX_FILL = '#fef3c7'
|
||||
|
||||
|
||||
def base_canvas(title):
|
||||
fig, ax = plt.subplots(figsize=(8.5, 8.5))
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title(title, fontsize=12)
|
||||
return fig, ax
|
||||
|
||||
|
||||
def draw_lift(ax, Gp, pos, A, B, other_surv, f_cols, e_cols, f_labels):
|
||||
"""G' with f_k and partial F_v boundary edges coloured; rest in light gray."""
|
||||
pentagon_edges = set()
|
||||
for k in range(5):
|
||||
pentagon_edges.add(frozenset((A[k], B[k]))) # f_k
|
||||
pentagon_edges.add(frozenset((B[k], B[(k + 1) % 5]))) # boundary
|
||||
for u, v in Gp.edges():
|
||||
if frozenset((u, v)) in pentagon_edges:
|
||||
continue
|
||||
(x0, y0), (x1, y1) = pos[u], pos[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=LIGHT, lw=1.2, zorder=1)
|
||||
other = [v for v in other_surv if v not in A and v not in B]
|
||||
xs = [pos[v][0] for v in other]
|
||||
ys = [pos[v][1] for v in other]
|
||||
ax.scatter(xs, ys, s=50, color=LIGHT, zorder=2)
|
||||
for k, v in enumerate(A):
|
||||
ax.scatter(*pos[v], s=240, color=DEG2, edgecolors='black',
|
||||
linewidths=0.8, zorder=4)
|
||||
ax.annotate(f'$A_{k}$', (pos[v][0] * 1.18, pos[v][1] * 1.18),
|
||||
ha='center', va='center', fontsize=12, color='#a16207',
|
||||
zorder=5)
|
||||
for k, v in enumerate(B):
|
||||
ax.scatter(*pos[v], s=160, color='white', edgecolors=DARK,
|
||||
linewidths=1.2, zorder=4)
|
||||
ax.annotate(f'$B_{k}$', (pos[v][0] * 0.62, pos[v][1] * 0.62),
|
||||
ha='center', va='center', fontsize=11, color=DARK,
|
||||
zorder=5)
|
||||
for k in range(5):
|
||||
(x0, y0), (x1, y1) = pos[B[k]], pos[A[k]]
|
||||
ax.plot([x0, x1], [y0, y1], color=f_cols[k], lw=3.2, zorder=3)
|
||||
mx, my = 0.55 * x0 + 0.45 * x1, 0.55 * y0 + 0.45 * y1
|
||||
ax.annotate(f_labels[k], (mx, my), fontsize=10, color=f_cols[k],
|
||||
fontweight='bold', ha='center', va='center', zorder=7,
|
||||
bbox=dict(boxstyle='round,pad=0.15', fc='white',
|
||||
ec=f_cols[k], lw=0.5))
|
||||
for k in range(5):
|
||||
(x0, y0), (x1, y1) = pos[B[k]], pos[B[(k + 1) % 5]]
|
||||
ax.plot([x0, x1], [y0, y1], color=e_cols[k], lw=3.2, zorder=3)
|
||||
|
||||
|
||||
def main():
|
||||
Gp, pos, Fv = build_dual()
|
||||
res = apply_reduction(Gp, pos, Fv, i=0)
|
||||
npos, A = res['pos'], res['A']
|
||||
v_n, apex_nbrs, chord = res['v_n'], res['apex_nbrs'], res['chord']
|
||||
|
||||
a_pair = {}
|
||||
for u in A:
|
||||
for nbr in Gp.neighbors(u):
|
||||
if nbr in Fv:
|
||||
a_pair[u] = nbr
|
||||
break
|
||||
B = [a_pair[A[k]] for k in range(5)]
|
||||
|
||||
survivors = [v for v in Gp if v not in Fv]
|
||||
surv_set = set(survivors)
|
||||
surv_edges = [(u, v) for u, v in Gp.edges()
|
||||
if u in surv_set and v in surv_set]
|
||||
other_surv = [v for v in survivors if v not in A]
|
||||
|
||||
# ----- Step 1: assumed colouring of the reduced dual -----
|
||||
fig, ax = base_canvas(
|
||||
"Step 1: $\\varphi$ on $\\widehat{G}'_{v,0}$ assigns distinct colours "
|
||||
"$X, Y, Z$ to the $v_n$-edges (propriety at $v_n$);\n"
|
||||
"by hypothesis $W \\neq Y$, forcing $W \\in \\{X, Z\\}$.")
|
||||
for u, v in surv_edges:
|
||||
(x0, y0), (x1, y1) = pos[u], pos[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=GRAY, lw=1.4, zorder=1)
|
||||
xs = [pos[v][0] for v in other_surv]
|
||||
ys = [pos[v][1] for v in other_surv]
|
||||
ax.scatter(xs, ys, s=80, color=DARK, zorder=3)
|
||||
for k, v in enumerate(A):
|
||||
ax.scatter(*pos[v], s=240, color=DEG2, edgecolors='black',
|
||||
linewidths=0.8, zorder=4)
|
||||
ax.annotate(f'$A_{k}$', (pos[v][0] * 1.18, pos[v][1] * 1.18),
|
||||
ha='center', va='center', fontsize=12, color='#a16207',
|
||||
zorder=5)
|
||||
for k, (color, lbl) in enumerate([(C1, '$X$'), (C2, '$Y$'), (C3, '$Z$')]):
|
||||
u = A[k]
|
||||
(x0, y0), (x1, y1) = npos[v_n], pos[u]
|
||||
ax.plot([x0, x1], [y0, y1], color=color, lw=3.4, zorder=5)
|
||||
mx, my = 0.5 * x1 + 0.5 * x0, 0.5 * y1 + 0.5 * y0
|
||||
ax.annotate(lbl, (mx, my), fontsize=12, color=color, fontweight='bold',
|
||||
ha='center', va='center', zorder=7,
|
||||
bbox=dict(boxstyle='round,pad=0.18', fc='white', ec=color,
|
||||
lw=0.6))
|
||||
ax.scatter(*npos[v_n], s=300, color=APEX_FILL, marker='s',
|
||||
edgecolors='black', linewidths=1.0, zorder=6)
|
||||
ax.annotate('$v_n$', npos[v_n], textcoords='offset points', xytext=(0, 14),
|
||||
ha='center', fontsize=12, fontweight='bold', color=DARK,
|
||||
zorder=7)
|
||||
(x0, y0), (x1, y1) = pos[chord[0]], pos[chord[1]]
|
||||
ax.plot([x0, x1], [y0, y1], color=DARK, lw=2.4, ls='--', zorder=5)
|
||||
mx, my = (x0 + x1) / 2, (y0 + y1) / 2
|
||||
ax.annotate('$W$', (mx, my), fontsize=12, color=DARK, fontweight='bold',
|
||||
ha='center', va='center', zorder=7,
|
||||
bbox=dict(boxstyle='round,pad=0.18', fc='white', ec=DARK,
|
||||
lw=0.6))
|
||||
ax.legend(handles=[
|
||||
Line2D([0], [0], color=C1, lw=3, label='$X$ (side)'),
|
||||
Line2D([0], [0], color=C2, lw=3, label='$Y$ (spike)'),
|
||||
Line2D([0], [0], color=C3, lw=3, label='$Z$ (side)'),
|
||||
Line2D([0], [0], color=DARK, lw=2, ls='--',
|
||||
label='$W$ (merged), $W \\neq Y$'),
|
||||
], loc='upper left', fontsize=10)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_chord_apex_step1.png'),
|
||||
dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
cases = [
|
||||
dict(
|
||||
title=("Step 2: lift to $G'$ when $W = Z$. The externals inherit "
|
||||
"$\\psi(f) = (X, Y, Z, Z, Z)$;\n"
|
||||
"Lemma 2.4 colours the five edges of $\\partial F_v$."),
|
||||
f_cols=[C1, C2, C3, C3, C3],
|
||||
e_cols=[C3, C1, C2, C1, C2],
|
||||
f_labels=['$X$', '$Y$', '$Z$', '$Z$', '$Z$'],
|
||||
out='fig_chord_apex_step2.png',
|
||||
),
|
||||
dict(
|
||||
title=("Step 3: lift to $G'$ when $W = X$. The externals inherit "
|
||||
"$\\psi(f) = (X, Y, Z, X, X)$;\n"
|
||||
"Lemma 2.4 colours the five edges of $\\partial F_v$."),
|
||||
f_cols=[C1, C2, C3, C1, C1],
|
||||
e_cols=[C3, C1, C2, C3, C2],
|
||||
f_labels=['$X$', '$Y$', '$Z$', '$X$', '$X$'],
|
||||
out='fig_chord_apex_step3.png',
|
||||
),
|
||||
]
|
||||
|
||||
for case in cases:
|
||||
fig, ax = base_canvas(case['title'])
|
||||
draw_lift(ax, Gp, pos, A, B, other_surv,
|
||||
case['f_cols'], case['e_cols'], case['f_labels'])
|
||||
fig.savefig(os.path.join(OUT_DIR, case['out']),
|
||||
dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f"wrote fig_chord_apex_step{{1,2,3}}.png to {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Draw a 3-panel illustration of cubic-graph edge contraction:
|
||||
(1) the original cubic graph fragment with edge e = uv highlighted;
|
||||
(2) after deleting e (u, v are degree-2);
|
||||
(3) after smoothing u, v (gone, replaced by single edges).
|
||||
|
||||
Produces fig_cubic_edge_contraction.png.
|
||||
"""
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import FancyArrowPatch
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
DARK = '#374151'
|
||||
GRAY = '#9ca3af'
|
||||
HIGHLIGHT = '#dc2626' # the edge being contracted (panel 1)
|
||||
GHOST = '#fca5a5' # removed edges (panel 2)
|
||||
DEG2 = '#f59e0b' # degree-2 vertices (panel 2)
|
||||
NEW = '#2563eb' # smoothed-in new edges (panel 3)
|
||||
|
||||
|
||||
# Positions: u centered at (-1, 0), v at (1, 0); their outer neighbors angled.
|
||||
pos = {
|
||||
'u': (-1.0, 0.0),
|
||||
'v': ( 1.0, 0.0),
|
||||
'a': (-2.2, 1.0),
|
||||
'b': (-2.2, -1.0),
|
||||
'c': ( 2.2, 1.0),
|
||||
'd': ( 2.2, -1.0),
|
||||
}
|
||||
|
||||
|
||||
def draw_vertex(ax, p, color, size=110, label=None, label_offset=(0, 0.22)):
|
||||
ax.scatter([p[0]], [p[1]], s=size, color=color, zorder=4)
|
||||
if label is not None:
|
||||
ax.text(p[0] + label_offset[0], p[1] + label_offset[1], label,
|
||||
ha='center', va='center', fontsize=12, zorder=5,
|
||||
color=DARK)
|
||||
|
||||
|
||||
def draw_edge(ax, p, q, color, lw=2.0, ls='-', zorder=2):
|
||||
ax.plot([p[0], q[0]], [p[1], q[1]], color=color, lw=lw, ls=ls,
|
||||
zorder=zorder, solid_capstyle='round')
|
||||
|
||||
|
||||
def panel_before(ax):
|
||||
# Outer edges (gray)
|
||||
for (x, y) in [('a', 'u'), ('b', 'u'), ('c', 'v'), ('d', 'v')]:
|
||||
draw_edge(ax, pos[x], pos[y], DARK, lw=2.0)
|
||||
# The highlighted edge e = uv
|
||||
draw_edge(ax, pos['u'], pos['v'], HIGHLIGHT, lw=3.2)
|
||||
# Vertices
|
||||
for v in ('a', 'b', 'c', 'd'):
|
||||
draw_vertex(ax, pos[v], DARK, size=60)
|
||||
draw_vertex(ax, pos['u'], DARK, size=120, label='$u$',
|
||||
label_offset=(-0.05, 0.28))
|
||||
draw_vertex(ax, pos['v'], DARK, size=120, label='$v$',
|
||||
label_offset=(0.05, 0.28))
|
||||
# Label on the edge
|
||||
mid = ((pos['u'][0] + pos['v'][0]) / 2,
|
||||
(pos['u'][1] + pos['v'][1]) / 2)
|
||||
ax.text(mid[0], mid[1] + 0.25, '$e$', ha='center', va='center',
|
||||
fontsize=13, color=HIGHLIGHT, zorder=5)
|
||||
ax.set_title('(1) cubic plane graph with edge $e = uv$',
|
||||
fontsize=11, color=DARK, pad=8)
|
||||
|
||||
|
||||
def panel_after_delete(ax):
|
||||
# Outer edges (gray)
|
||||
for (x, y) in [('a', 'u'), ('b', 'u'), ('c', 'v'), ('d', 'v')]:
|
||||
draw_edge(ax, pos[x], pos[y], DARK, lw=2.0)
|
||||
# Ghost the deleted edge
|
||||
draw_edge(ax, pos['u'], pos['v'], GHOST, lw=2.0, ls=':')
|
||||
# Vertices: u, v are now degree-2 (highlighted color)
|
||||
for v in ('a', 'b', 'c', 'd'):
|
||||
draw_vertex(ax, pos[v], DARK, size=60)
|
||||
draw_vertex(ax, pos['u'], DEG2, size=140, label='$u$',
|
||||
label_offset=(-0.05, 0.32))
|
||||
draw_vertex(ax, pos['v'], DEG2, size=140, label='$v$',
|
||||
label_offset=(0.05, 0.32))
|
||||
ax.set_title('(2) delete $e$: $u, v$ now have degree $2$',
|
||||
fontsize=11, color=DARK, pad=8)
|
||||
|
||||
|
||||
def panel_after_smooth(ax):
|
||||
# The smoothed-in new edges
|
||||
draw_edge(ax, pos['a'], pos['b'], NEW, lw=3.0)
|
||||
draw_edge(ax, pos['c'], pos['d'], NEW, lw=3.0)
|
||||
# Outer vertices remain
|
||||
for v in ('a', 'b', 'c', 'd'):
|
||||
draw_vertex(ax, pos[v], DARK, size=60)
|
||||
# u, v are gone — show their former positions as faint markers
|
||||
ax.scatter([pos['u'][0], pos['v'][0]], [pos['u'][1], pos['v'][1]],
|
||||
s=140, facecolors='none', edgecolors=GRAY, lw=1.0,
|
||||
linestyles='--', zorder=3)
|
||||
ax.text(pos['u'][0], pos['u'][1] + 0.32, '$u$ gone',
|
||||
ha='center', va='center', fontsize=9, color=GRAY)
|
||||
ax.text(pos['v'][0], pos['v'][1] + 0.32, '$v$ gone',
|
||||
ha='center', va='center', fontsize=9, color=GRAY)
|
||||
ax.set_title('(3) smooth $u, v$: their incident edges merge',
|
||||
fontsize=11, color=DARK, pad=8)
|
||||
|
||||
|
||||
def main():
|
||||
fig, axes = plt.subplots(1, 3, figsize=(13.5, 4.2))
|
||||
for ax in axes:
|
||||
ax.set_xlim(-3.0, 3.0)
|
||||
ax.set_ylim(-1.7, 1.7)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
panel_before(axes[0])
|
||||
panel_after_delete(axes[1])
|
||||
panel_after_smooth(axes[2])
|
||||
plt.subplots_adjust(left=0.02, right=0.98, top=0.92, bottom=0.04,
|
||||
wspace=0.05)
|
||||
out = os.path.join(OUT_DIR, 'fig_cubic_edge_contraction.png')
|
||||
plt.savefig(out, dpi=180, bbox_inches='tight')
|
||||
print(f"wrote {out}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,464 @@
|
||||
"""Lift the recoloured + bridged H_1 back to G' (dual of the n=14
|
||||
triangulation), producing a proper 3-edge-colouring of the modified G'.
|
||||
|
||||
The modified G' = G' with: subdivisions Y_1, Y_2 of the two green witness
|
||||
edges, plus a new red edge Y_1-Y_2.
|
||||
|
||||
Run with: sage experiments/draw_lift_to_Gprime.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
import matplotlib.pyplot as plt
|
||||
import math
|
||||
import os
|
||||
|
||||
|
||||
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
|
||||
"""Same Tutte barycentric layout as in draw_iterated_reduction_n14.py."""
|
||||
avoid = set(avoid_verts or ())
|
||||
candidates = []
|
||||
for face in G_sage.faces():
|
||||
verts = [u for (u, v) in face]
|
||||
if not (set(verts) & avoid):
|
||||
candidates.append(verts)
|
||||
if not candidates:
|
||||
outer = [u for (u, v) in max(G_sage.faces(), key=len)]
|
||||
else:
|
||||
outer = max(candidates, key=len)
|
||||
n_outer = len(outer)
|
||||
pos = {}
|
||||
for k, v in enumerate(outer):
|
||||
ang = 2 * math.pi * k / n_outer + math.pi / 2
|
||||
pos[v] = (math.cos(ang), math.sin(ang))
|
||||
interior = [v for v in G_sage.vertex_iterator() if v not in pos]
|
||||
for v in interior: pos[v] = (0.0, 0.0)
|
||||
for _ in range(iterations):
|
||||
new_pos = dict(pos)
|
||||
for v in interior:
|
||||
nbrs = list(G_sage.neighbor_iterator(v))
|
||||
sx = sum(pos[w][0] for w in nbrs) / len(nbrs)
|
||||
sy = sum(pos[w][1] for w in nbrs) / len(nbrs)
|
||||
new_pos[v] = (sx, sy)
|
||||
pos = new_pos
|
||||
return pos
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
C = ['#dc2626', '#16a34a', '#2563eb']
|
||||
DARK = '#374151'
|
||||
HIGHLIGHT = '#fef3c7'
|
||||
|
||||
|
||||
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 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_set(edges, col_list, start_idx, color_pair):
|
||||
a, b = color_pair
|
||||
if col_list[start_idx] not in (a, b): return set()
|
||||
in_sub = set(i for i in range(len(edges)) if col_list[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 trace_kempe_cycle(edges, col_list, start_idx, color_pair):
|
||||
cycle_set = kempe_cycle_set(edges, col_list, start_idx, color_pair)
|
||||
incident_at = {}
|
||||
for ei in cycle_set:
|
||||
u, v = edges[ei][0], edges[ei][1]
|
||||
incident_at.setdefault(u, []).append(ei)
|
||||
incident_at.setdefault(v, []).append(ei)
|
||||
start_u, start_v = edges[start_idx][0], edges[start_idx][1]
|
||||
walk = [(start_idx, start_v)]
|
||||
cur_e = start_idx
|
||||
cur_leave = start_v
|
||||
while True:
|
||||
nbrs = incident_at[cur_leave]
|
||||
if len(nbrs) != 2: break
|
||||
nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1]
|
||||
u2, v2 = edges[nxt][0], edges[nxt][1]
|
||||
leave_next = v2 if u2 == cur_leave else u2
|
||||
if nxt == start_idx: break
|
||||
walk.append((nxt, leave_next))
|
||||
cur_e = nxt
|
||||
cur_leave = leave_next
|
||||
return walk
|
||||
|
||||
|
||||
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_set(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_set(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 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, 'A': A, 'boundary': boundary, 'face': face, 'i_red': i,
|
||||
'named': {
|
||||
'spike': frozenset(spike),
|
||||
'side_0': frozenset(side_0),
|
||||
'side_1': frozenset(side_1),
|
||||
'merged': frozenset(merged),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def find_first_match():
|
||||
for G in graphs.triangulations(14, minimum_degree=5):
|
||||
if not G.is_planar(set_embedding=True): continue
|
||||
D = dual_of(G); D.is_planar(set_embedding=True)
|
||||
for face in D.faces():
|
||||
if len(face) != 5: continue
|
||||
for i_red in range(5):
|
||||
res = apply_reduction(D, face, i_red, '__v_n_1__')
|
||||
if res is None: continue
|
||||
H, named = res['H'], res['named']
|
||||
edges, gen = proper_3_edge_colorings(H)
|
||||
for col in gen:
|
||||
if matches_chord_apex_kempe(edges, col, named):
|
||||
coloring_dict = {frozenset((e[0], e[1])): c
|
||||
for e, c in zip(edges, col)}
|
||||
return G, D, res, H, coloring_dict
|
||||
return None
|
||||
|
||||
|
||||
def find_conj_witness(H, edges, col_list, named):
|
||||
GREEN, BLUE = 1, 2
|
||||
merged_idx = edge_idx(edges, named['merged'])
|
||||
kc_gb = kempe_cycle_set(edges, col_list, merged_idx, (GREEN, BLUE))
|
||||
if merged_idx not in kc_gb: return None
|
||||
for face in H.faces():
|
||||
face_edge_ids = []
|
||||
for u, v in face:
|
||||
ei = edge_idx(edges, frozenset((u, v)))
|
||||
if ei is not None:
|
||||
face_edge_ids.append(ei)
|
||||
green_on_face_in_kc = [ei for ei in face_edge_ids
|
||||
if col_list[ei] == GREEN
|
||||
and ei in kc_gb
|
||||
and ei != merged_idx]
|
||||
if len(green_on_face_in_kc) >= 2:
|
||||
return face, green_on_face_in_kc[0], green_on_face_in_kc[1], kc_gb
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("Setting up ...")
|
||||
G14, D, red_info, H, coloring = find_first_match()
|
||||
i_red = red_info['i_red']
|
||||
boundary_in_D = red_info['boundary']
|
||||
A_in_D = red_info['A']
|
||||
named_in_D = red_info['named']
|
||||
print(f" Found: i_red = {i_red}")
|
||||
print(f" G' has |V|={D.order()}, |E|={D.size()}")
|
||||
|
||||
# Relabel H so all vertices are integers
|
||||
H_relabel_map = {v: i for i, v in enumerate(H.vertex_iterator())}
|
||||
inv_relabel = {i: v for v, i in H_relabel_map.items()}
|
||||
H.relabel(perm=H_relabel_map, inplace=True)
|
||||
coloring = {frozenset(H_relabel_map[u] for u in e): c
|
||||
for e, c in coloring.items()}
|
||||
named_H = {role: frozenset(H_relabel_map[u] for u in e)
|
||||
for role, e in named_in_D.items()}
|
||||
|
||||
H.is_planar(set_embedding=True)
|
||||
edges_H = list(H.edges(labels=False))
|
||||
col_list = [coloring[frozenset((u, v))] for (u, v) in edges_H]
|
||||
witness = find_conj_witness(H, edges_H, col_list, named_H)
|
||||
face_w, e1, e2, kc_gb = witness
|
||||
e1_uv = tuple(edges_H[e1]); e2_uv = tuple(edges_H[e2])
|
||||
print(f" Witness in H_1 (relabeled): e1 = {e1_uv}, e2 = {e2_uv}")
|
||||
|
||||
e1_D = (inv_relabel[e1_uv[0]], inv_relabel[e1_uv[1]])
|
||||
e2_D = (inv_relabel[e2_uv[0]], inv_relabel[e2_uv[1]])
|
||||
print(f" Witness in D labels: e1 = {e1_D}, e2 = {e2_D}")
|
||||
|
||||
# Build modified G'
|
||||
G_mod = D.copy()
|
||||
max_label = max(v for v in D.vertices(sort=False) if isinstance(v, int))
|
||||
Y1 = max_label + 1
|
||||
Y2 = Y1 + 1
|
||||
G_mod.add_vertex(Y1); G_mod.add_vertex(Y2)
|
||||
G_mod.delete_edge(e1_D); G_mod.delete_edge(e2_D)
|
||||
G_mod.add_edges([(e1_D[0], Y1), (Y1, e1_D[1]),
|
||||
(e2_D[0], Y2), (Y2, e2_D[1]),
|
||||
(Y1, Y2)])
|
||||
assert G_mod.is_planar(set_embedding=True)
|
||||
print(f" Modified G': |V|={G_mod.order()}, |E|={G_mod.size()}")
|
||||
|
||||
# Trace Kempe cycle from merged
|
||||
merged_idx = edge_idx(edges_H, named_H['merged'])
|
||||
walk = trace_kempe_cycle(edges_H, col_list, merged_idx, (1, 2))
|
||||
walk_edges = [w[0] for w in walk]
|
||||
leave_at = [w[1] for w in walk]
|
||||
|
||||
# Map relabeled-H vertex -> D label if not v_n_1, else None
|
||||
def H_to_D(v):
|
||||
d = inv_relabel[v]
|
||||
return d if d != '__v_n_1__' else None
|
||||
|
||||
# Build new coloring on G_mod's edges.
|
||||
# Step A: copy non-named, non-e1, non-e2, non-v_n_1 edges' original colors.
|
||||
new_coloring = {}
|
||||
named_H_set = set(named_H.values())
|
||||
for e_fs, c in coloring.items():
|
||||
if e_fs in named_H_set: continue
|
||||
if e_fs == frozenset(e1_uv) or e_fs == frozenset(e2_uv): continue
|
||||
u, v = tuple(e_fs)
|
||||
du, dv = H_to_D(u), H_to_D(v)
|
||||
if du is None or dv is None: continue
|
||||
new_coloring[frozenset((du, dv))] = c
|
||||
|
||||
# Step B: walk K' (subdivided cycle) starting from merged.
|
||||
# For each old cycle edge that is NOT spike/side_0/side_1/merged
|
||||
# (i.e., not involving v_n_1, not the chord), we OVERWRITE its color
|
||||
# via the alternation. For e1, e2 we instead emit the two subdivision
|
||||
# halves. For named edges (spike, side_1, merged), they don't exist
|
||||
# in G_mod, so we record the would-be color to use later for f-vector.
|
||||
pos = 0
|
||||
cycle_label = {} # e_fs in D labels -> new color (for cycle edges in G_mod)
|
||||
half_label = {} # halves' colors
|
||||
would_be = {} # named role -> would-be color after recoloring
|
||||
for k, ei in enumerate(walk_edges):
|
||||
leaving_vertex = leave_at[k]
|
||||
u, v = edges_H[ei][0], edges_H[ei][1]
|
||||
entry_vertex = v if leaving_vertex == u else u
|
||||
e_fs_H = frozenset((u, v))
|
||||
if ei == e1:
|
||||
c0 = 2 if pos % 2 == 0 else 1; pos += 1
|
||||
c1 = 2 if pos % 2 == 0 else 1; pos += 1
|
||||
half_label[frozenset((inv_relabel[entry_vertex], Y1))] = c0
|
||||
half_label[frozenset((Y1, inv_relabel[leaving_vertex]))] = c1
|
||||
elif ei == e2:
|
||||
c0 = 2 if pos % 2 == 0 else 1; pos += 1
|
||||
c1 = 2 if pos % 2 == 0 else 1; pos += 1
|
||||
half_label[frozenset((inv_relabel[entry_vertex], Y2))] = c0
|
||||
half_label[frozenset((Y2, inv_relabel[leaving_vertex]))] = c1
|
||||
else:
|
||||
c = 2 if pos % 2 == 0 else 1; pos += 1
|
||||
# is this a named edge (spike/side_1/merged)? Note: it can't be
|
||||
# side_0 since side_0 is red and the cycle is green/blue.
|
||||
for role, ef in named_H.items():
|
||||
if ef == e_fs_H:
|
||||
would_be[role] = c
|
||||
break
|
||||
else:
|
||||
# Regular cycle edge: convert to D labels
|
||||
du, dv = H_to_D(u), H_to_D(v)
|
||||
if du is None or dv is None:
|
||||
# Shouldn't happen for non-named edges
|
||||
pass
|
||||
else:
|
||||
cycle_label[frozenset((du, dv))] = c
|
||||
|
||||
# Apply cycle relabeling
|
||||
for e_fs, c in cycle_label.items():
|
||||
new_coloring[e_fs] = c
|
||||
# Apply half colors
|
||||
for e_fs, c in half_label.items():
|
||||
new_coloring[e_fs] = c
|
||||
# New red edge
|
||||
new_coloring[frozenset((Y1, Y2))] = 0
|
||||
|
||||
print(f" Would-be colors of named H_1 edges after recolor: {would_be}")
|
||||
# would_be may not include side_0 (not on cycle); side_0's color is
|
||||
# whatever it was in original (= red = 0).
|
||||
if 'side_0' not in would_be:
|
||||
would_be['side_0'] = coloring[named_H['side_0']] # 0 (red)
|
||||
|
||||
# Step C: color the 5 externals f_k = B_k - A_k.
|
||||
# At each A_k, the third color = would_be color of the missing H_1 edge.
|
||||
def third(c1, c2): return ({0, 1, 2} - {c1, c2}).pop()
|
||||
externals_colors = [None] * 5
|
||||
for k, B_k in enumerate(boundary_in_D):
|
||||
A_k = A_in_D[k]
|
||||
# Which named role was at A_k in H_1?
|
||||
# A_in_D[i_red] = side_0's endpoint, A_in_D[i_red+1] = spike's,
|
||||
# A_in_D[i_red+2] = side_1's, A_in_D[i_red+3,+4] = merged.
|
||||
if k == i_red: role = 'side_0'
|
||||
elif k == (i_red+1) % 5: role = 'spike'
|
||||
elif k == (i_red+2) % 5: role = 'side_1'
|
||||
else: role = 'merged'
|
||||
c_ext = would_be[role]
|
||||
new_coloring[frozenset((B_k, A_k))] = c_ext
|
||||
externals_colors[k] = c_ext
|
||||
print(f" f-vector at F_v: {externals_colors}")
|
||||
|
||||
# Step D: 5 boundary edges B_k - B_{k+1} via Lemma 2.4.
|
||||
boundary_colors = [None] * 5
|
||||
for k in range(5):
|
||||
f_k = externals_colors[k]
|
||||
f_kp1 = externals_colors[(k + 1) % 5]
|
||||
if f_k != f_kp1:
|
||||
boundary_colors[k] = third(f_k, f_kp1)
|
||||
for _ in range(20):
|
||||
changed = False
|
||||
for k in range(5):
|
||||
if boundary_colors[k] is not None: continue
|
||||
f_k = externals_colors[k]
|
||||
prev_c = boundary_colors[(k - 1) % 5]
|
||||
next_c = boundary_colors[(k + 1) % 5]
|
||||
forbidden = {f_k}
|
||||
if prev_c is not None: forbidden.add(prev_c)
|
||||
if next_c is not None: forbidden.add(next_c)
|
||||
allowed = list({0, 1, 2} - forbidden)
|
||||
if allowed:
|
||||
boundary_colors[k] = allowed[0]
|
||||
changed = True
|
||||
if not changed: break
|
||||
|
||||
for k in range(5):
|
||||
if boundary_colors[k] is None:
|
||||
print(f" WARNING: undetermined boundary color at k={k}")
|
||||
return
|
||||
B_k = boundary_in_D[k]; B_kp1 = boundary_in_D[(k + 1) % 5]
|
||||
new_coloring[frozenset((B_k, B_kp1))] = boundary_colors[k]
|
||||
|
||||
# Sanity check
|
||||
bad = []
|
||||
for v in G_mod.vertices(sort=False):
|
||||
seen = []
|
||||
for w in G_mod.neighbor_iterator(v):
|
||||
seen.append(new_coloring.get(frozenset((v, w))))
|
||||
if None in seen or len(set(seen)) != len(seen):
|
||||
bad.append((v, seen))
|
||||
if bad:
|
||||
print(f" PROPRIETY FAILS at vertices:")
|
||||
for v, s in bad[:5]:
|
||||
print(f" {v}: {s}")
|
||||
else:
|
||||
print(" Propriety holds at every vertex of modified G'.")
|
||||
|
||||
# Use H_1's Tutte layout for vertices shared between H_1 and G' (the 19
|
||||
# surviving vertices). Then add the 5 boundary vertices B_k of partial F_v
|
||||
# by running a local barycenter iteration with the shared vertices fixed.
|
||||
H.is_planar(set_embedding=True)
|
||||
H_pos = tutte_layout(H,
|
||||
avoid_verts={H_relabel_map['__v_n_1__']})
|
||||
# Map H_1 positions back to D labels (skip v_n_1)
|
||||
pos_layout = {}
|
||||
for v_H, p in H_pos.items():
|
||||
v_D = inv_relabel[v_H]
|
||||
if v_D != '__v_n_1__':
|
||||
pos_layout[v_D] = p
|
||||
# Place B_k's by iterating barycenter of their 3 neighbours in G_mod
|
||||
# (B_{k-1}, B_{k+1}, A_k); start each B_k near its A_k.
|
||||
B_pos = {B_k: pos_layout[A_in_D[k]]
|
||||
for k, B_k in enumerate(boundary_in_D)}
|
||||
for _ in range(300):
|
||||
new_B = {}
|
||||
for k, B_k in enumerate(boundary_in_D):
|
||||
B_prev = boundary_in_D[(k - 1) % 5]
|
||||
B_next = boundary_in_D[(k + 1) % 5]
|
||||
A_k = A_in_D[k]
|
||||
sx = (B_pos[B_prev][0] + B_pos[B_next][0] + pos_layout[A_k][0]) / 3
|
||||
sy = (B_pos[B_prev][1] + B_pos[B_next][1] + pos_layout[A_k][1]) / 3
|
||||
new_B[B_k] = (sx, sy)
|
||||
B_pos = new_B
|
||||
for B_k, p in B_pos.items():
|
||||
pos_layout[B_k] = p
|
||||
# Y_1, Y_2 at midpoints of their subdivided edges e1, e2
|
||||
pos_layout[Y1] = ((pos_layout[e1_D[0]][0] + pos_layout[e1_D[1]][0]) / 2,
|
||||
(pos_layout[e1_D[0]][1] + pos_layout[e1_D[1]][1]) / 2)
|
||||
pos_layout[Y2] = ((pos_layout[e2_D[0]][0] + pos_layout[e2_D[1]][0]) / 2,
|
||||
(pos_layout[e2_D[0]][1] + pos_layout[e2_D[1]][1]) / 2)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
for u, v, _ in G_mod.edges():
|
||||
e = frozenset((u, v))
|
||||
c = C[new_coloring[e]]
|
||||
lw = 3.4 if e == frozenset((Y1, Y2)) else 1.5
|
||||
(x0, y0), (x1, y1) = pos_layout[u], pos_layout[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
|
||||
for v in G_mod.vertices(sort=False):
|
||||
x, y = pos_layout[v]
|
||||
if v in (Y1, Y2):
|
||||
ax.scatter(x, y, s=140, color=DARK, edgecolors='white',
|
||||
linewidths=1.6, zorder=6)
|
||||
else:
|
||||
ax.scatter(x, y, s=55, color=DARK, zorder=3)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
out_path = os.path.join(OUT_DIR, 'fig_lift_to_Gprime.png')
|
||||
fig.savefig(out_path, dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Draw the four steps of the reduced-dual construction (Definition 2.1).
|
||||
|
||||
Uses the dodecahedron G' = dual of the icosahedron, with F_v the inner pentagon,
|
||||
as built in reduced_dual.py. Produces fig_reduced_dual_step{1..4}.png.
|
||||
"""
|
||||
import os
|
||||
import math
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Polygon
|
||||
from matplotlib.lines import Line2D
|
||||
|
||||
from reduced_dual import build_dual, apply_reduction
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
GRAY = '#9ca3af'
|
||||
DARK = '#374151'
|
||||
GHOST = '#fca5a5'
|
||||
DEG2 = '#f59e0b'
|
||||
APEX = '#16a34a'
|
||||
CHORD = '#2563eb'
|
||||
FACE = '#fef9c3'
|
||||
|
||||
|
||||
def draw_edges(ax, G, pos, nodes=None, **kw):
|
||||
for u, v in G.edges():
|
||||
if nodes is not None and (u not in nodes or v not in nodes):
|
||||
continue
|
||||
(x0, y0), (x1, y1) = pos[u], pos[v]
|
||||
ax.plot([x0, x1], [y0, y1], **kw)
|
||||
|
||||
|
||||
def draw_nodes(ax, pos, nodes, **kw):
|
||||
xs = [pos[v][0] for v in nodes]
|
||||
ys = [pos[v][1] for v in nodes]
|
||||
ax.scatter(xs, ys, **kw)
|
||||
|
||||
|
||||
def face_F_polygon(pos):
|
||||
"""The new central face F: decagon alternating b_i, c_i clockwise."""
|
||||
order = []
|
||||
for i in range(5):
|
||||
order += [('b', i), ('c', i)]
|
||||
return [pos[v] for v in order]
|
||||
|
||||
|
||||
def base_canvas(title):
|
||||
fig, ax = plt.subplots(figsize=(8.5, 8.5))
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title(title, fontsize=12)
|
||||
return fig, ax
|
||||
|
||||
|
||||
def main():
|
||||
Gp, pos, Fv = build_dual()
|
||||
res = apply_reduction(Gp, pos, Fv, i=0)
|
||||
Ghat, npos, A = res['Ghat'], res['pos'], res['A']
|
||||
v_n, apex_nbrs, chord = res['v_n'], res['apex_nbrs'], res['chord']
|
||||
|
||||
survivors = [v for v in Gp if v not in Fv] # b, c, d families
|
||||
surv_set = set(survivors)
|
||||
deg2 = list(A) # the five b_i
|
||||
|
||||
# surviving edges (both endpoints survive) vs deleted edges (touch an a_i)
|
||||
surv_edges = [(u, v) for u, v in Gp.edges()
|
||||
if u in surv_set and v in surv_set]
|
||||
del_edges = [(u, v) for u, v in Gp.edges()
|
||||
if u not in surv_set or v not in surv_set]
|
||||
|
||||
def draw_surviving(ax):
|
||||
ax.add_patch(Polygon(face_F_polygon(pos), closed=True,
|
||||
facecolor=FACE, edgecolor='none', zorder=0))
|
||||
for u, v in surv_edges:
|
||||
(x0, y0), (x1, y1) = pos[u], pos[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=GRAY, lw=1.6, zorder=1)
|
||||
others = [v for v in survivors if v not in deg2]
|
||||
draw_nodes(ax, pos, others, s=120, color=DARK, zorder=3)
|
||||
|
||||
def draw_ghosts(ax):
|
||||
for u, v in del_edges:
|
||||
(x0, y0), (x1, y1) = pos[u], pos[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=GHOST, lw=1.2, ls='--', zorder=1)
|
||||
draw_nodes(ax, pos, Fv, s=120, color='white', edgecolors=GHOST,
|
||||
linewidths=1.5, zorder=2)
|
||||
for v in Fv:
|
||||
ax.plot(*pos[v], marker='x', color=GHOST, ms=8, zorder=3)
|
||||
|
||||
# ----- Step 1: delete F_v's boundary; five degree-2 vertices on face F -----
|
||||
fig, ax = base_canvas(
|
||||
"Step 1: delete the five dual vertices on $\\partial F_v$.\n"
|
||||
"Their outer neighbours drop to degree 2 (orange) and lie on a new "
|
||||
"face $F$ (shaded).")
|
||||
draw_surviving(ax)
|
||||
draw_ghosts(ax)
|
||||
draw_nodes(ax, pos, deg2, s=260, color=DEG2, edgecolors='black',
|
||||
linewidths=1.0, zorder=4)
|
||||
cx = sum(pos[('a', i)][0] for i in range(5)) / 5
|
||||
cy = sum(pos[('a', i)][1] for i in range(5)) / 5
|
||||
ax.text(cx, cy, '$F$', fontsize=16, ha='center', va='center',
|
||||
color='#a16207', zorder=5)
|
||||
ax.legend(handles=[
|
||||
Line2D([0], [0], marker='x', color=GHOST, lw=0, label='deleted (was $\\partial F_v$)'),
|
||||
Line2D([0], [0], marker='o', color='w', markerfacecolor=DEG2,
|
||||
markeredgecolor='black', label='degree-2 vertex'),
|
||||
], loc='upper left', fontsize=10)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_reduced_dual_step1.png'),
|
||||
dpi=170, bbox_inches='tight'); plt.close(fig)
|
||||
|
||||
# ----- Step 2: order the five degree-2 vertices clockwise as A_0..A_4 -----
|
||||
fig, ax = base_canvas(
|
||||
"Step 2: list the degree-2 vertices clockwise around $F$ as "
|
||||
"$A_0,\\dots,A_4$.")
|
||||
draw_surviving(ax)
|
||||
draw_nodes(ax, pos, deg2, s=300, color=DEG2, edgecolors='black',
|
||||
linewidths=1.0, zorder=4)
|
||||
for k, v in enumerate(A):
|
||||
x, y = pos[v]
|
||||
ax.annotate(f'$A_{k}$', (x, y), textcoords='offset points',
|
||||
xytext=(0, 0), ha='center', va='center', fontsize=10,
|
||||
fontweight='bold', color='black', zorder=5)
|
||||
# outward label too
|
||||
ax.annotate(f'$A_{k}$', (x * 1.18, y * 1.18), ha='center', va='center',
|
||||
fontsize=12, color='#a16207', zorder=5)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_reduced_dual_step2.png'),
|
||||
dpi=170, bbox_inches='tight'); plt.close(fig)
|
||||
|
||||
# ----- Step 3: add v_n joined to A_i, A_{i+1}, A_{i+2} -----
|
||||
fig, ax = base_canvas(
|
||||
"Step 3: add a vertex $v_n$ joined to $A_i, A_{i+1}, A_{i+2}$ "
|
||||
"(here $i=0$).")
|
||||
draw_surviving(ax)
|
||||
draw_nodes(ax, pos, deg2, s=300, color=DEG2, edgecolors='black',
|
||||
linewidths=1.0, zorder=4)
|
||||
for k, v in enumerate(A):
|
||||
ax.annotate(f'$A_{k}$', (pos[v][0] * 1.18, pos[v][1] * 1.18),
|
||||
ha='center', va='center', fontsize=12, color='#a16207', zorder=5)
|
||||
for u in apex_nbrs:
|
||||
(x0, y0), (x1, y1) = npos[v_n], pos[u]
|
||||
ax.plot([x0, x1], [y0, y1], color=APEX, lw=2.4, zorder=5)
|
||||
draw_nodes(ax, npos, [v_n], s=320, color=APEX, marker='s',
|
||||
edgecolors='black', linewidths=1.0, zorder=6)
|
||||
ax.annotate('$v_n$', npos[v_n], textcoords='offset points', xytext=(0, 14),
|
||||
ha='center', fontsize=12, fontweight='bold', color=APEX, zorder=7)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_reduced_dual_step3.png'),
|
||||
dpi=170, bbox_inches='tight'); plt.close(fig)
|
||||
|
||||
# ----- Step 4: add chord A_{i+3} A_{i+4}; the reduced dual -----
|
||||
fig, ax = base_canvas(
|
||||
"Step 4: add the edge $A_{i+3} A_{i+4}$. The result $\\widehat{G}'_{v,i}$ "
|
||||
"is again cubic and planar.")
|
||||
draw_surviving(ax)
|
||||
draw_nodes(ax, pos, deg2, s=300, color=DEG2, edgecolors='black',
|
||||
linewidths=1.0, zorder=4)
|
||||
for k, v in enumerate(A):
|
||||
ax.annotate(f'$A_{k}$', (pos[v][0] * 1.18, pos[v][1] * 1.18),
|
||||
ha='center', va='center', fontsize=12, color='#a16207', zorder=5)
|
||||
for u in apex_nbrs:
|
||||
(x0, y0), (x1, y1) = npos[v_n], pos[u]
|
||||
ax.plot([x0, x1], [y0, y1], color=APEX, lw=2.4, zorder=5)
|
||||
draw_nodes(ax, npos, [v_n], s=320, color=APEX, marker='s',
|
||||
edgecolors='black', linewidths=1.0, zorder=6)
|
||||
ax.annotate('$v_n$', npos[v_n], textcoords='offset points', xytext=(0, 14),
|
||||
ha='center', fontsize=12, fontweight='bold', color=APEX, zorder=7)
|
||||
(x0, y0), (x1, y1) = pos[chord[0]], pos[chord[1]]
|
||||
ax.plot([x0, x1], [y0, y1], color=CHORD, lw=2.8, zorder=5)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_reduced_dual_step4.png'),
|
||||
dpi=170, bbox_inches='tight'); plt.close(fig)
|
||||
|
||||
print("wrote fig_reduced_dual_step1..4.png to", OUT_DIR)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,277 @@
|
||||
"""Draw a Conjecture 3.6 witness: on H_1 with its chord-apex+Kempe coloring,
|
||||
find a face with two green edges that lie (with the merged edge) on a common
|
||||
{green, blue}-Kempe cycle. Subdivide both green edges with new vertices and
|
||||
join the two new vertices by a new red edge.
|
||||
|
||||
Run with: sage experiments/draw_step1_conj36.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Polygon
|
||||
import math
|
||||
import os
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
C = ['#dc2626', '#16a34a', '#2563eb'] # 0=red 1=green 2=blue
|
||||
GRAY = '#9ca3af'
|
||||
DARK = '#374151'
|
||||
HIGHLIGHT = '#fef3c7'
|
||||
|
||||
|
||||
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, 'A': A,
|
||||
'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 find_first_match():
|
||||
for G in graphs.triangulations(14, minimum_degree=5):
|
||||
if not G.is_planar(set_embedding=True): continue
|
||||
D = dual_of(G); D.is_planar(set_embedding=True)
|
||||
for face in D.faces():
|
||||
if len(face) != 5: continue
|
||||
for i_red in range(5):
|
||||
res = apply_reduction(D, face, i_red, '__v_n_1__')
|
||||
if res is None: continue
|
||||
H, named = res['H'], res['named']
|
||||
edges, gen = proper_3_edge_colorings(H)
|
||||
for col in gen:
|
||||
if matches_chord_apex_kempe(edges, col, named):
|
||||
coloring_dict = {frozenset((e[0], e[1])): c
|
||||
for e, c in zip(edges, col)}
|
||||
return G, D, face, i_red, H, named, coloring_dict
|
||||
return None
|
||||
|
||||
|
||||
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
|
||||
avoid = set(avoid_verts or ())
|
||||
candidates = []
|
||||
for face in G_sage.faces():
|
||||
verts = [u for (u, v) in face]
|
||||
if not (set(verts) & avoid):
|
||||
candidates.append(verts)
|
||||
if not candidates:
|
||||
outer = [u for (u, v) in max(G_sage.faces(), key=len)]
|
||||
else:
|
||||
outer = max(candidates, key=len)
|
||||
n_outer = len(outer)
|
||||
pos = {}
|
||||
for k, v in enumerate(outer):
|
||||
ang = 2 * math.pi * k / n_outer + math.pi / 2
|
||||
pos[v] = (math.cos(ang), math.sin(ang))
|
||||
interior = [v for v in G_sage.vertex_iterator() if v not in pos]
|
||||
for v in interior: pos[v] = (0.0, 0.0)
|
||||
for _ in range(iterations):
|
||||
new_pos = dict(pos)
|
||||
for v in interior:
|
||||
nbrs = list(G_sage.neighbor_iterator(v))
|
||||
sx = sum(pos[w][0] for w in nbrs) / len(nbrs)
|
||||
sy = sum(pos[w][1] for w in nbrs) / len(nbrs)
|
||||
new_pos[v] = (sx, sy)
|
||||
pos = new_pos
|
||||
return pos
|
||||
|
||||
|
||||
def find_conj_witness(H, edges, col_list, named):
|
||||
"""Find a face F of H with two distinct green edges e1, e2, NEITHER equal
|
||||
to the merged edge, such that e1, e2, merged all lie on the
|
||||
{green, blue}-Kempe cycle through merged."""
|
||||
GREEN, BLUE = 1, 2
|
||||
merged_idx = edge_idx(edges, named['merged'])
|
||||
kc_gb = kempe_cycle(edges, col_list, merged_idx, (GREEN, BLUE))
|
||||
if merged_idx not in kc_gb:
|
||||
return None
|
||||
for face in H.faces():
|
||||
face_edge_ids = []
|
||||
for u, v in face:
|
||||
ei = edge_idx(edges, frozenset((u, v)))
|
||||
if ei is not None:
|
||||
face_edge_ids.append(ei)
|
||||
green_on_face_in_kc = [ei for ei in face_edge_ids
|
||||
if col_list[ei] == GREEN
|
||||
and ei in kc_gb
|
||||
and ei != merged_idx]
|
||||
if len(green_on_face_in_kc) >= 2:
|
||||
return face, green_on_face_in_kc[0], green_on_face_in_kc[1], kc_gb
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("Searching for the first n=14 chord-apex+Kempe match ...")
|
||||
result = find_first_match()
|
||||
G14, D, face_chosen, i_red, H, named, coloring = result
|
||||
print(f" Found: i_red = {i_red}")
|
||||
|
||||
H_relabel_map = {v: i for i, v in enumerate(H.vertex_iterator())}
|
||||
H.relabel(perm=H_relabel_map, inplace=True)
|
||||
vn = H_relabel_map['__v_n_1__']
|
||||
coloring = {frozenset(H_relabel_map[u] for u in e): c
|
||||
for e, c in coloring.items()}
|
||||
named = {role: frozenset(H_relabel_map[u] for u in e)
|
||||
for role, e in named.items()}
|
||||
|
||||
H.is_planar(set_embedding=True)
|
||||
pos = tutte_layout(H, avoid_verts={vn})
|
||||
E_protected = set(named.values())
|
||||
|
||||
# Build (edges, coloring) in list/tuple form to use kempe helpers
|
||||
edges = list(H.edges(labels=False))
|
||||
col_list = [coloring[frozenset((u, v))] for (u, v) in edges]
|
||||
|
||||
witness = find_conj_witness(H, edges, col_list, named)
|
||||
if witness is None:
|
||||
print("ERROR: no witness found.")
|
||||
return
|
||||
face_w, e1, e2, kc_gb = witness
|
||||
e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2])
|
||||
print(f" Witness face has {len(face_w)} edges.")
|
||||
print(f" e1 = {e1_uv}, e2 = {e2_uv}")
|
||||
print(f" {{green, blue}}-Kempe cycle through merged: {len(kc_gb)} edges.")
|
||||
|
||||
# Midpoints in the layout
|
||||
mp1 = ((pos[e1_uv[0]][0] + pos[e1_uv[1]][0]) / 2,
|
||||
(pos[e1_uv[0]][1] + pos[e1_uv[1]][1]) / 2)
|
||||
mp2 = ((pos[e2_uv[0]][0] + pos[e2_uv[1]][0]) / 2,
|
||||
(pos[e2_uv[0]][1] + pos[e2_uv[1]][1]) / 2)
|
||||
|
||||
# Draw
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
for u, v, _ in H.edges():
|
||||
e = frozenset([u, v])
|
||||
c = C[coloring[e]]
|
||||
lw = 3.8 if e in E_protected else 1.4
|
||||
(x0, y0), (x1, y1) = pos[u], pos[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
|
||||
for v in H.vertices(sort=False):
|
||||
x, y = pos[v]
|
||||
if v == vn:
|
||||
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
|
||||
edgecolors='black', linewidths=1.2, zorder=4)
|
||||
ax.annotate('$v_n^{(1)}$', (x, y),
|
||||
textcoords='offset points', xytext=(16, 16),
|
||||
ha='left', fontsize=14, fontweight='bold',
|
||||
color=DARK, zorder=6,
|
||||
bbox=dict(boxstyle='round,pad=0.2', fc='white',
|
||||
ec=DARK, lw=0.6))
|
||||
else:
|
||||
ax.scatter(x, y, s=70, color=DARK, zorder=3)
|
||||
|
||||
# New red edge between midpoints
|
||||
ax.plot([mp1[0], mp2[0]], [mp1[1], mp2[1]],
|
||||
color=C[0], lw=4.0, zorder=5)
|
||||
# New vertices
|
||||
for (mx, my) in (mp1, mp2):
|
||||
ax.scatter(mx, my, s=130, color=DARK, edgecolors='white',
|
||||
linewidths=1.6, zorder=6)
|
||||
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
out_path = os.path.join(OUT_DIR, 'fig_alg_step1_conj36.png')
|
||||
fig.savefig(out_path, dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,373 @@
|
||||
"""Build the Conjecture 3.6 witness on H_1, subdivide the two green edges
|
||||
with new vertices X1, X2, add a new red edge X1-X2, then recolor the
|
||||
{green, blue}-Kempe cycle through merged so that walking it from merged
|
||||
gives the alternating sequence blue, green, blue, green, ... (equivalently,
|
||||
a Kempe swap of green/blue along the cycle).
|
||||
|
||||
Edges off the cycle keep their original colour; the new red edge stays red.
|
||||
Propriety holds throughout: at every old vertex the 2 cycle edges still
|
||||
alternate green/blue and the off-cycle edge is red; at X1 and X2 the two
|
||||
cycle halves are forced to opposite colours of {green, blue} and the new
|
||||
red edge supplies the third colour.
|
||||
|
||||
Run with: sage experiments/draw_step1_conj36_recolored.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Polygon
|
||||
import math
|
||||
import os
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
C = ['#dc2626', '#16a34a', '#2563eb'] # 0=red 1=green 2=blue
|
||||
GRAY = '#9ca3af'
|
||||
DARK = '#374151'
|
||||
HIGHLIGHT = '#fef3c7'
|
||||
|
||||
|
||||
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, 'A': A,
|
||||
'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(edges, col_list, start_idx, color_pair):
|
||||
a, b = color_pair
|
||||
if col_list[start_idx] not in (a, b): return set()
|
||||
in_sub = set(i for i in range(len(edges)) if col_list[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 trace_kempe_cycle(edges, col_list, start_idx, color_pair):
|
||||
"""Walk the (a,b)-Kempe cycle through edges[start_idx]. Returns a list
|
||||
of (edge_idx, direction) where direction is the vertex you LEAVE the
|
||||
edge by. The returned list is in cyclic order, length 2L."""
|
||||
a, b = color_pair
|
||||
cycle_edges = kempe_cycle_edges(edges, col_list, start_idx, color_pair)
|
||||
# build adjacency: at each vertex on the cycle, the 2 (a,b)-edges incident
|
||||
incident_at = {}
|
||||
for ei in cycle_edges:
|
||||
u, v = edges[ei][0], edges[ei][1]
|
||||
incident_at.setdefault(u, []).append(ei)
|
||||
incident_at.setdefault(v, []).append(ei)
|
||||
# walk
|
||||
start_u, start_v = edges[start_idx][0], edges[start_idx][1]
|
||||
walk = [(start_idx, start_v)]
|
||||
cur_e = start_idx
|
||||
cur_leave = start_v
|
||||
while True:
|
||||
# at cur_leave, the 2 cycle edges are cur_e and the next one
|
||||
nbrs = incident_at[cur_leave]
|
||||
if len(nbrs) != 2: break
|
||||
nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1]
|
||||
u2, v2 = edges[nxt][0], edges[nxt][1]
|
||||
leave_next = v2 if u2 == cur_leave else u2
|
||||
if nxt == start_idx: break
|
||||
walk.append((nxt, leave_next))
|
||||
cur_e = nxt
|
||||
cur_leave = leave_next
|
||||
return walk
|
||||
|
||||
|
||||
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(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(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 find_first_match():
|
||||
for G in graphs.triangulations(14, minimum_degree=5):
|
||||
if not G.is_planar(set_embedding=True): continue
|
||||
D = dual_of(G); D.is_planar(set_embedding=True)
|
||||
for face in D.faces():
|
||||
if len(face) != 5: continue
|
||||
for i_red in range(5):
|
||||
res = apply_reduction(D, face, i_red, '__v_n_1__')
|
||||
if res is None: continue
|
||||
H, named = res['H'], res['named']
|
||||
edges, gen = proper_3_edge_colorings(H)
|
||||
for col in gen:
|
||||
if matches_chord_apex_kempe(edges, col, named):
|
||||
coloring_dict = {frozenset((e[0], e[1])): c
|
||||
for e, c in zip(edges, col)}
|
||||
return G, D, face, i_red, H, named, coloring_dict
|
||||
return None
|
||||
|
||||
|
||||
def tutte_layout(G_sage, avoid_verts=None, iterations=300):
|
||||
avoid = set(avoid_verts or ())
|
||||
candidates = []
|
||||
for face in G_sage.faces():
|
||||
verts = [u for (u, v) in face]
|
||||
if not (set(verts) & avoid):
|
||||
candidates.append(verts)
|
||||
if not candidates:
|
||||
outer = [u for (u, v) in max(G_sage.faces(), key=len)]
|
||||
else:
|
||||
outer = max(candidates, key=len)
|
||||
n_outer = len(outer)
|
||||
pos = {}
|
||||
for k, v in enumerate(outer):
|
||||
ang = 2 * math.pi * k / n_outer + math.pi / 2
|
||||
pos[v] = (math.cos(ang), math.sin(ang))
|
||||
interior = [v for v in G_sage.vertex_iterator() if v not in pos]
|
||||
for v in interior: pos[v] = (0.0, 0.0)
|
||||
for _ in range(iterations):
|
||||
new_pos = dict(pos)
|
||||
for v in interior:
|
||||
nbrs = list(G_sage.neighbor_iterator(v))
|
||||
sx = sum(pos[w][0] for w in nbrs) / len(nbrs)
|
||||
sy = sum(pos[w][1] for w in nbrs) / len(nbrs)
|
||||
new_pos[v] = (sx, sy)
|
||||
pos = new_pos
|
||||
return pos
|
||||
|
||||
|
||||
def find_conj_witness(H, edges, col_list, named):
|
||||
GREEN, BLUE = 1, 2
|
||||
merged_idx = edge_idx(edges, named['merged'])
|
||||
kc_gb = kempe_cycle_edges(edges, col_list, merged_idx, (GREEN, BLUE))
|
||||
if merged_idx not in kc_gb:
|
||||
return None
|
||||
for face in H.faces():
|
||||
face_edge_ids = []
|
||||
for u, v in face:
|
||||
ei = edge_idx(edges, frozenset((u, v)))
|
||||
if ei is not None:
|
||||
face_edge_ids.append(ei)
|
||||
green_on_face_in_kc = [ei for ei in face_edge_ids
|
||||
if col_list[ei] == GREEN
|
||||
and ei in kc_gb
|
||||
and ei != merged_idx]
|
||||
if len(green_on_face_in_kc) >= 2:
|
||||
return face, green_on_face_in_kc[0], green_on_face_in_kc[1], kc_gb
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("Setting up ...")
|
||||
result = find_first_match()
|
||||
G14, D, face_chosen, i_red, H, named, coloring = result
|
||||
print(f" Found: i_red = {i_red}")
|
||||
|
||||
H_relabel_map = {v: i for i, v in enumerate(H.vertex_iterator())}
|
||||
H.relabel(perm=H_relabel_map, inplace=True)
|
||||
vn = H_relabel_map['__v_n_1__']
|
||||
coloring = {frozenset(H_relabel_map[u] for u in e): c
|
||||
for e, c in coloring.items()}
|
||||
named = {role: frozenset(H_relabel_map[u] for u in e)
|
||||
for role, e in named.items()}
|
||||
|
||||
H.is_planar(set_embedding=True)
|
||||
pos = tutte_layout(H, avoid_verts={vn})
|
||||
|
||||
edges = list(H.edges(labels=False))
|
||||
col_list = [coloring[frozenset((u, v))] for (u, v) in edges]
|
||||
|
||||
witness = find_conj_witness(H, edges, col_list, named)
|
||||
face_w, e1, e2, kc_gb = witness
|
||||
e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2])
|
||||
merged_idx = edge_idx(edges, named['merged'])
|
||||
print(f" Witness: e1 = {e1_uv}, e2 = {e2_uv}, |kc_gb| = {len(kc_gb)}")
|
||||
|
||||
# Trace the Kempe cycle starting from merged
|
||||
walk = trace_kempe_cycle(edges, col_list, merged_idx, (1, 2))
|
||||
walk_edges_in_order = [w[0] for w in walk]
|
||||
leave_in_order = [w[1] for w in walk]
|
||||
print(f" Kempe cycle (in order from merged): length {len(walk_edges_in_order)}")
|
||||
|
||||
# Compute midpoints
|
||||
mp1 = ((pos[e1_uv[0]][0] + pos[e1_uv[1]][0]) / 2,
|
||||
(pos[e1_uv[0]][1] + pos[e1_uv[1]][1]) / 2)
|
||||
mp2 = ((pos[e2_uv[0]][0] + pos[e2_uv[1]][0]) / 2,
|
||||
(pos[e2_uv[0]][1] + pos[e2_uv[1]][1]) / 2)
|
||||
|
||||
# Construct the new graph: H with e1, e2 subdivided and new red edge X1-X2.
|
||||
H_new = H.copy()
|
||||
X1 = max(H.vertices(sort=False)) + 1
|
||||
X2 = X1 + 1
|
||||
H_new.add_vertex(X1); H_new.add_vertex(X2)
|
||||
H_new.delete_edge(e1_uv)
|
||||
H_new.delete_edge(e2_uv)
|
||||
e1_a, e1_b = e1_uv
|
||||
e2_a, e2_b = e2_uv
|
||||
H_new.add_edges([(e1_a, X1), (X1, e1_b),
|
||||
(e2_a, X2), (X2, e2_b),
|
||||
(X1, X2)])
|
||||
pos_new = dict(pos); pos_new[X1] = mp1; pos_new[X2] = mp2
|
||||
|
||||
# Build subdivided cycle K' as a list of (edge_frozenset, "entry vertex
|
||||
# in original direction") in cyclic order starting from merged.
|
||||
K_prime = []
|
||||
for k, ei in enumerate(walk_edges_in_order):
|
||||
leaving_vertex = leave_in_order[k]
|
||||
u, v = edges[ei][0], edges[ei][1]
|
||||
entry_vertex = v if leaving_vertex == u else u
|
||||
if ei == e1:
|
||||
K_prime.append(frozenset((entry_vertex, X1)))
|
||||
K_prime.append(frozenset((X1, leaving_vertex)))
|
||||
elif ei == e2:
|
||||
K_prime.append(frozenset((entry_vertex, X2)))
|
||||
K_prime.append(frozenset((X2, leaving_vertex)))
|
||||
else:
|
||||
K_prime.append(frozenset((u, v)))
|
||||
|
||||
print(f" Subdivided cycle length: {len(K_prime)}")
|
||||
|
||||
# Recolor K' alternately starting from merged = blue
|
||||
new_coloring = dict(coloring)
|
||||
new_coloring.pop(frozenset(e1_uv), None)
|
||||
new_coloring.pop(frozenset(e2_uv), None)
|
||||
for k, e_fs in enumerate(K_prime):
|
||||
new_coloring[e_fs] = 2 if k % 2 == 0 else 1 # blue, green, blue, green
|
||||
# New red edge X1-X2
|
||||
new_coloring[frozenset((X1, X2))] = 0
|
||||
|
||||
# Sanity: check propriety at every vertex of H_new
|
||||
bad = False
|
||||
for v in H_new.vertices(sort=False):
|
||||
seen = []
|
||||
for w in H_new.neighbor_iterator(v):
|
||||
seen.append(new_coloring[frozenset((v, w))])
|
||||
if len(set(seen)) != len(seen):
|
||||
print(f" *** propriety FAILS at vertex {v}: colors {seen}")
|
||||
bad = True
|
||||
if not bad:
|
||||
print(" Propriety holds at every vertex.")
|
||||
|
||||
# Identify which edges of H_new are "protected" (the original named edges,
|
||||
# minus e1, e2 which have been subdivided away).
|
||||
E_protected = set()
|
||||
for role, ef in named.items():
|
||||
if ef in (frozenset(e1_uv), frozenset(e2_uv)):
|
||||
continue
|
||||
E_protected.add(ef)
|
||||
|
||||
# Draw
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
for u, v, _ in H_new.edges():
|
||||
e = frozenset([u, v])
|
||||
c = C[new_coloring[e]]
|
||||
if e == frozenset((X1, X2)):
|
||||
lw = 4.0
|
||||
elif e in E_protected:
|
||||
lw = 3.8
|
||||
else:
|
||||
lw = 1.4
|
||||
(x0, y0), (x1, y1) = pos_new[u], pos_new[v]
|
||||
ax.plot([x0, x1], [y0, y1], color=c, lw=lw, zorder=2)
|
||||
for v in H_new.vertices(sort=False):
|
||||
x, y = pos_new[v]
|
||||
if v == vn:
|
||||
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
|
||||
edgecolors='black', linewidths=1.2, zorder=4)
|
||||
ax.annotate('$v_n^{(1)}$', (x, y),
|
||||
textcoords='offset points', xytext=(16, 16),
|
||||
ha='left', fontsize=14, fontweight='bold',
|
||||
color=DARK, zorder=6,
|
||||
bbox=dict(boxstyle='round,pad=0.2', fc='white',
|
||||
ec=DARK, lw=0.6))
|
||||
elif v in (X1, X2):
|
||||
ax.scatter(x, y, s=130, color=DARK, edgecolors='white',
|
||||
linewidths=1.6, zorder=6)
|
||||
else:
|
||||
ax.scatter(x, y, s=70, color=DARK, zorder=3)
|
||||
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
out_path = os.path.join(OUT_DIR, 'fig_alg_step1_conj36_recolored.png')
|
||||
fig.savefig(out_path, dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f"Wrote {out_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Two-panel illustration of Theorem (cubic contraction across a 4-face).
|
||||
|
||||
Left: H near the 4-face, with the forced 3-edge-colouring
|
||||
e_0=a, e_1=b, e_2=e_3=c, w_0=w_1=b, w_2=w_3=a.
|
||||
Right: H' after cubic-graph edge contraction on e_0, with the new colouring
|
||||
(e_2', e_3' both b; e_1 recoloured to c; everything else unchanged).
|
||||
|
||||
Produces fig_thm_cubic_contraction_4face.png.
|
||||
"""
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
DARK = '#374151'
|
||||
GRAY = '#9ca3af'
|
||||
GHOST = '#fca5a5'
|
||||
DEG2 = '#f59e0b'
|
||||
|
||||
# Colour code: a=orange-ish, b=blue, c=green. Chosen colourblind-friendly.
|
||||
COL_A = '#ea580c' # 'a'
|
||||
COL_B = '#2563eb' # 'b'
|
||||
COL_C = '#16a34a' # 'c'
|
||||
|
||||
# Positions of the 4-face vertices and their external neighbours.
|
||||
pos = {
|
||||
'v0': (0.0, 0.0),
|
||||
'v1': (2.4, 0.0),
|
||||
'v2': (2.4, 2.4),
|
||||
'v3': (0.0, 2.4),
|
||||
'u0': (-1.5, -0.8),
|
||||
'u1': ( 3.9, -0.8),
|
||||
'u2': ( 3.9, 3.2),
|
||||
'u3': (-1.5, 3.2),
|
||||
}
|
||||
|
||||
|
||||
def draw_edge(ax, p, q, color, lw=2.4, ls='-', zorder=2):
|
||||
ax.plot([p[0], q[0]], [p[1], q[1]], color=color, lw=lw, ls=ls,
|
||||
zorder=zorder, solid_capstyle='round')
|
||||
|
||||
|
||||
def draw_vertex(ax, p, color=DARK, size=70, zorder=4):
|
||||
ax.scatter([p[0]], [p[1]], s=size, color=color, zorder=zorder)
|
||||
|
||||
|
||||
def label_vertex(ax, p, text, offset=(0.0, 0.28), fontsize=12, color=DARK):
|
||||
ax.text(p[0] + offset[0], p[1] + offset[1], text,
|
||||
ha='center', va='center', fontsize=fontsize, color=color,
|
||||
zorder=5)
|
||||
|
||||
|
||||
def label_edge(ax, p, q, text, offset=(0.0, 0.0), color=DARK, fontsize=11):
|
||||
mid = ((p[0] + q[0]) / 2 + offset[0], (p[1] + q[1]) / 2 + offset[1])
|
||||
ax.text(mid[0], mid[1], text, ha='center', va='center',
|
||||
fontsize=fontsize, color=color, zorder=5,
|
||||
bbox=dict(boxstyle='round,pad=0.15', facecolor='white',
|
||||
edgecolor='none', alpha=0.85))
|
||||
|
||||
|
||||
def shade_face(ax, vs, color='#fef9c3', alpha=0.7):
|
||||
xs = [p[0] for p in vs] + [vs[0][0]]
|
||||
ys = [p[1] for p in vs] + [vs[0][1]]
|
||||
ax.fill(xs, ys, color=color, alpha=alpha, zorder=1)
|
||||
|
||||
|
||||
def panel_before(ax):
|
||||
# 4-face shading
|
||||
shade_face(ax, [pos['v0'], pos['v1'], pos['v2'], pos['v3']])
|
||||
# Face label
|
||||
ax.text(1.2, 1.2, '$f$', ha='center', va='center', fontsize=14,
|
||||
color=GRAY, style='italic', zorder=2)
|
||||
|
||||
# Face edges
|
||||
draw_edge(ax, pos['v0'], pos['v1'], COL_A) # e_0 = a
|
||||
draw_edge(ax, pos['v2'], pos['v3'], COL_B) # e_1 = b
|
||||
draw_edge(ax, pos['v1'], pos['v2'], COL_C) # e_2 = c
|
||||
draw_edge(ax, pos['v3'], pos['v0'], COL_C) # e_3 = c
|
||||
# External edges
|
||||
draw_edge(ax, pos['v0'], pos['u0'], COL_B) # w_0 = b
|
||||
draw_edge(ax, pos['v1'], pos['u1'], COL_B) # w_1 = b
|
||||
draw_edge(ax, pos['v2'], pos['u2'], COL_A) # w_2 = a
|
||||
draw_edge(ax, pos['v3'], pos['u3'], COL_A) # w_3 = a
|
||||
# Vertices
|
||||
for v in ('v0', 'v1', 'v2', 'v3'):
|
||||
draw_vertex(ax, pos[v], DARK, size=90)
|
||||
for u in ('u0', 'u1', 'u2', 'u3'):
|
||||
draw_vertex(ax, pos[u], DARK, size=60)
|
||||
|
||||
# Labels
|
||||
label_vertex(ax, pos['v0'], '$v_0$', offset=(-0.20, -0.25))
|
||||
label_vertex(ax, pos['v1'], '$v_1$', offset=( 0.20, -0.25))
|
||||
label_vertex(ax, pos['v2'], '$v_2$', offset=( 0.20, 0.25))
|
||||
label_vertex(ax, pos['v3'], '$v_3$', offset=(-0.20, 0.25))
|
||||
label_vertex(ax, pos['u0'], '$u_0$', offset=(-0.25, 0.00))
|
||||
label_vertex(ax, pos['u1'], '$u_1$', offset=( 0.25, 0.00))
|
||||
label_vertex(ax, pos['u2'], '$u_2$', offset=( 0.25, 0.00))
|
||||
label_vertex(ax, pos['u3'], '$u_3$', offset=(-0.25, 0.00))
|
||||
|
||||
# Edge labels with colour
|
||||
label_edge(ax, pos['v0'], pos['v1'], r'$e_0\!=\!a$', offset=(0, -0.18),
|
||||
color=COL_A)
|
||||
label_edge(ax, pos['v2'], pos['v3'], r'$e_1\!=\!b$', offset=(0, 0.18),
|
||||
color=COL_B)
|
||||
label_edge(ax, pos['v1'], pos['v2'], r'$e_2\!=\!c$', offset=(0.30, 0.0),
|
||||
color=COL_C)
|
||||
label_edge(ax, pos['v3'], pos['v0'], r'$e_3\!=\!c$', offset=(-0.30, 0.0),
|
||||
color=COL_C)
|
||||
label_edge(ax, pos['v0'], pos['u0'], r'$w_0\!=\!b$',
|
||||
offset=(-0.05, -0.05), color=COL_B, fontsize=10)
|
||||
label_edge(ax, pos['v1'], pos['u1'], r'$w_1\!=\!b$',
|
||||
offset=( 0.05, -0.05), color=COL_B, fontsize=10)
|
||||
label_edge(ax, pos['v2'], pos['u2'], r'$w_2\!=\!a$',
|
||||
offset=( 0.05, 0.05), color=COL_A, fontsize=10)
|
||||
label_edge(ax, pos['v3'], pos['u3'], r'$w_3\!=\!a$',
|
||||
offset=(-0.05, 0.05), color=COL_A, fontsize=10)
|
||||
|
||||
ax.set_title('$H$ with proper $3$-edge-colouring $\\varphi$:\n'
|
||||
'$\\varphi(e_0)=a$, $\\varphi(e_1)=b$ (opposite, different)',
|
||||
fontsize=11, color=DARK, pad=10)
|
||||
|
||||
|
||||
def panel_after(ax):
|
||||
# New 'face' shading: now (v_2, v_3) connected via e_1, plus new edges
|
||||
# form a hexagonal-ish region. Just shade the area lightly.
|
||||
shade_face(ax,
|
||||
[pos['u0'], pos['v3'], pos['v2'], pos['u1'],
|
||||
(pos['u1'][0] + 0.7, pos['u1'][1] - 0.7),
|
||||
(pos['u0'][0] - 0.7, pos['u0'][1] - 0.7)],
|
||||
color='#fef9c3', alpha=0.0) # invisible, just spacing
|
||||
|
||||
# Ghost the deleted edges (e_0, e_2, e_3, w_0, w_1) and former vertices
|
||||
draw_edge(ax, pos['v0'], pos['v1'], GHOST, lw=1.5, ls=':') # e_0
|
||||
draw_edge(ax, pos['v1'], pos['v2'], GHOST, lw=1.5, ls=':') # e_2
|
||||
draw_edge(ax, pos['v3'], pos['v0'], GHOST, lw=1.5, ls=':') # e_3
|
||||
draw_edge(ax, pos['v0'], pos['u0'], GHOST, lw=1.5, ls=':') # w_0
|
||||
draw_edge(ax, pos['v1'], pos['u1'], GHOST, lw=1.5, ls=':') # w_1
|
||||
|
||||
# Surviving / recoloured edges
|
||||
draw_edge(ax, pos['v2'], pos['v3'], COL_C, lw=3.0) # e_1 recoloured to c
|
||||
draw_edge(ax, pos['v2'], pos['u2'], COL_A) # w_2 = a
|
||||
draw_edge(ax, pos['v3'], pos['u3'], COL_A) # w_3 = a
|
||||
|
||||
# Smoothed-in new edges: e_2' from v_2 to u_1, e_3' from v_3 to u_0
|
||||
draw_edge(ax, pos['v2'], pos['u1'], COL_B, lw=3.0)
|
||||
draw_edge(ax, pos['v3'], pos['u0'], COL_B, lw=3.0)
|
||||
|
||||
# Vertices: v_0, v_1 removed; show their former positions faintly
|
||||
for ghost in ('v0', 'v1'):
|
||||
ax.scatter([pos[ghost][0]], [pos[ghost][1]], s=120,
|
||||
facecolors='none', edgecolors=GRAY, lw=1.0,
|
||||
linestyles='--', zorder=3)
|
||||
for v in ('v2', 'v3'):
|
||||
draw_vertex(ax, pos[v], DARK, size=90)
|
||||
for u in ('u0', 'u1', 'u2', 'u3'):
|
||||
draw_vertex(ax, pos[u], DARK, size=60)
|
||||
|
||||
# Labels
|
||||
ax.text(pos['v0'][0], pos['v0'][1] - 0.30, '$v_0$ gone',
|
||||
ha='center', va='center', fontsize=9, color=GRAY)
|
||||
ax.text(pos['v1'][0], pos['v1'][1] - 0.30, '$v_1$ gone',
|
||||
ha='center', va='center', fontsize=9, color=GRAY)
|
||||
|
||||
label_vertex(ax, pos['v2'], '$v_2$', offset=( 0.20, 0.25))
|
||||
label_vertex(ax, pos['v3'], '$v_3$', offset=(-0.20, 0.25))
|
||||
label_vertex(ax, pos['u0'], '$u_0$', offset=(-0.25, 0.00))
|
||||
label_vertex(ax, pos['u1'], '$u_1$', offset=( 0.25, 0.00))
|
||||
label_vertex(ax, pos['u2'], '$u_2$', offset=( 0.25, 0.00))
|
||||
label_vertex(ax, pos['u3'], '$u_3$', offset=(-0.25, 0.00))
|
||||
|
||||
# Edge labels for the new/recoloured edges
|
||||
label_edge(ax, pos['v2'], pos['v3'], r'$e_1\!=\!c$', offset=(0, 0.18),
|
||||
color=COL_C)
|
||||
label_edge(ax, pos['v2'], pos['u1'], r"$e_2'\!=\!b$",
|
||||
offset=(0.20, 0.10), color=COL_B)
|
||||
label_edge(ax, pos['v3'], pos['u0'], r"$e_3'\!=\!b$",
|
||||
offset=(-0.20, 0.10), color=COL_B)
|
||||
label_edge(ax, pos['v2'], pos['u2'], r'$w_2\!=\!a$',
|
||||
offset=( 0.05, 0.05), color=COL_A, fontsize=10)
|
||||
label_edge(ax, pos['v3'], pos['u3'], r'$w_3\!=\!a$',
|
||||
offset=(-0.05, 0.05), color=COL_A, fontsize=10)
|
||||
|
||||
ax.set_title("$H'$ after cubic contraction of $e_0$:\n"
|
||||
r"$e_2', e_3'$ get colour $b$; $e_1$ recoloured to $c$",
|
||||
fontsize=11, color=DARK, pad=10)
|
||||
|
||||
|
||||
def main():
|
||||
fig, axes = plt.subplots(1, 2, figsize=(13, 5.8))
|
||||
for ax in axes:
|
||||
ax.set_xlim(-2.6, 4.8)
|
||||
ax.set_ylim(-2.0, 4.2)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
panel_before(axes[0])
|
||||
panel_after(axes[1])
|
||||
plt.subplots_adjust(left=0.02, right=0.98, top=0.92, bottom=0.04,
|
||||
wspace=0.05)
|
||||
out = os.path.join(OUT_DIR, 'fig_thm_cubic_contraction_4face.png')
|
||||
plt.savefig(out, dpi=180, bbox_inches='tight')
|
||||
print(f"wrote {out}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Iterated reduced-dual reduction with protected edges (Sage).
|
||||
|
||||
Implements Algorithm 3.1 from paper.tex:
|
||||
|
||||
0. G' = dual of G (a triangulation conjectured to be a minimal
|
||||
counterexample).
|
||||
1. Apply Definition 2.1 to G' at a pentagonal face to get H_1; find a
|
||||
proper 3-edge-colouring phi_1 of H_1.
|
||||
2. Initialise protected edge set E := {spike, side-0, side-1, merged} from
|
||||
step 1.
|
||||
3. Iterate: find a pentagonal face of H_{t-1} whose 10 incident edges are
|
||||
disjoint from E, pick a valid index i_t (Lemma 2.4), apply
|
||||
Definition 2.1, extend phi_{t-1} to phi_t, and add the four new named
|
||||
edges to E.
|
||||
4. Terminate when no safe pentagonal face exists.
|
||||
|
||||
We run on the icosahedron-dodecahedron pair as a concrete trace; the
|
||||
icosahedron is 4-colourable, so the dodecahedron is 3-edge-colourable and
|
||||
the algorithm terminates combinatorially.
|
||||
|
||||
Run with:
|
||||
sage experiments/iterated_reduction.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_coloring import edge_coloring
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build G' = dodecahedron with the same naming as experiments/reduced_dual.py:
|
||||
# vertex families a (inner pentagon, will play the role of partial F_v's
|
||||
# boundary vertices for the first reduction), b, c, d.
|
||||
# ---------------------------------------------------------------------------
|
||||
def build_dodecahedron():
|
||||
edges = []
|
||||
for i in range(5):
|
||||
edges.append((('a', i), ('a', (i + 1) % 5))) # inner pentagon
|
||||
edges.append((('a', i), ('b', i))) # spoke a-b
|
||||
edges.append((('b', i), ('c', i))) # b-c
|
||||
edges.append((('b', i), ('c', (i - 1) % 5))) # b-c (other side)
|
||||
edges.append((('c', i), ('d', i))) # spoke c-d
|
||||
edges.append((('d', i), ('d', (i + 1) % 5))) # outer pentagon
|
||||
G = Graph(edges, multiedges=False, loops=False)
|
||||
assert G.is_planar(set_embedding=True), "dodecahedron not planar?"
|
||||
return G
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3-edge-colour a cubic plane graph; return None if not 3-edge-colourable.
|
||||
# ---------------------------------------------------------------------------
|
||||
def proper_3_coloring(G):
|
||||
classes = edge_coloring(G, value_only=False)
|
||||
if len(classes) > 3:
|
||||
return None
|
||||
out = {}
|
||||
for k, edge_list in enumerate(classes):
|
||||
for u, v in edge_list:
|
||||
out[frozenset([u, v])] = k
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Face search: pentagonal face whose 10 incident edges are outside `protected`.
|
||||
# Returns (boundary, externals, A) or None.
|
||||
# ---------------------------------------------------------------------------
|
||||
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]
|
||||
# the external edge at each boundary vertex (the one not on the face)
|
||||
externals = []
|
||||
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:
|
||||
# In a cubic graph each face-boundary vertex has exactly one
|
||||
# external edge; otherwise something is off.
|
||||
break
|
||||
externals.append(frozenset([B_k, outer[0]]))
|
||||
A.append(outer[0])
|
||||
else:
|
||||
if not any(e in protected for e in boundary_edges + externals):
|
||||
return boundary, externals, A
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lemma 2.4: any proper 3-edge-colouring forces the external vector at a
|
||||
# pentagonal face to have shape (a, b, c, c, c) up to cyclic rotation. The
|
||||
# valid index i for the reduction is one where positions i+3, i+4 host the
|
||||
# doubled colour and positions i, i+1, i+2 host three distinct colours.
|
||||
# ---------------------------------------------------------------------------
|
||||
def valid_indices(f_vec):
|
||||
out = []
|
||||
for i in range(5):
|
||||
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
|
||||
continue
|
||||
triple = {f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}
|
||||
if len(triple) == 3:
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apply Definition 2.1 at (F, i): delete the 5 boundary vertices, add v_n
|
||||
# connected to A[i], A[i+1], A[i+2], add the chord A[i+3]-A[i+4]. Extend the
|
||||
# colouring: every surviving edge keeps its colour; each new edge takes the
|
||||
# colour of the deleted external at the same A_k (the unique third colour at
|
||||
# A_k under the surviving edges).
|
||||
# ---------------------------------------------------------------------------
|
||||
def apply_reduction(G, boundary, externals, A, coloring, i, v_n_label):
|
||||
G_new = G.copy()
|
||||
for v in boundary:
|
||||
G_new.delete_vertex(v)
|
||||
G_new.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])
|
||||
G_new.add_edges([side_0, spike, side_1, merged])
|
||||
assert G_new.is_planar(set_embedding=True), "reduction broke planarity"
|
||||
|
||||
coloring_new = {
|
||||
e: c for e, c in coloring.items() if not any(u in boundary for u in e)
|
||||
}
|
||||
coloring_new[frozenset(side_0)] = coloring[externals[i]]
|
||||
coloring_new[frozenset(spike)] = coloring[externals[(i + 1) % 5]]
|
||||
coloring_new[frozenset(side_1)] = coloring[externals[(i + 2) % 5]]
|
||||
coloring_new[frozenset(merged)] = coloring[externals[(i + 3) % 5]]
|
||||
|
||||
named = {
|
||||
'spike': frozenset(spike),
|
||||
'side_0': frozenset(side_0),
|
||||
'side_1': frozenset(side_1),
|
||||
'merged': frozenset(merged),
|
||||
}
|
||||
return G_new, coloring_new, named
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sanity check: verify that a coloring is proper (each vertex sees 3 colours).
|
||||
# ---------------------------------------------------------------------------
|
||||
def is_proper_3_coloring(G, coloring):
|
||||
for v in G.vertex_iterator():
|
||||
seen = set()
|
||||
for u in G.neighbor_iterator(v):
|
||||
seen.add(coloring[frozenset([u, v])])
|
||||
if len(seen) != G.degree(v):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main driver.
|
||||
# ---------------------------------------------------------------------------
|
||||
def run_algorithm(max_iterations=50, verbose=True):
|
||||
if verbose:
|
||||
print("STEP 0: G' = dodecahedron (dual of the icosahedron)")
|
||||
G_prime = build_dodecahedron()
|
||||
if verbose:
|
||||
print(f" |V(G')| = {G_prime.order()}, |E(G')| = {G_prime.size()}")
|
||||
|
||||
# ---- STEP 1: apply Definition 2.1 to G' at the inner pentagon, i_1 = 0
|
||||
face_data = find_safe_pentagonal_face(G_prime, set())
|
||||
if face_data is None:
|
||||
print(" No pentagonal face in G'.")
|
||||
return
|
||||
boundary, externals, A = face_data
|
||||
|
||||
H = G_prime.copy()
|
||||
for v in boundary:
|
||||
H.delete_vertex(v)
|
||||
v_n_label = ('v_n', 1)
|
||||
H.add_vertex(v_n_label)
|
||||
side_0 = (v_n_label, A[0])
|
||||
spike = (v_n_label, A[1])
|
||||
side_1 = (v_n_label, A[2])
|
||||
merged = (A[3], A[4])
|
||||
H.add_edges([side_0, spike, side_1, merged])
|
||||
assert H.is_planar(set_embedding=True)
|
||||
|
||||
coloring = proper_3_coloring(H)
|
||||
if coloring is None:
|
||||
print(" H_1 is not 3-edge-colourable; algorithm halts.")
|
||||
return
|
||||
assert is_proper_3_coloring(H, coloring)
|
||||
if verbose:
|
||||
print(f"STEP 1: H_1 = reduced dual at the first face, i_1 = 0")
|
||||
print(f" |V(H_1)| = {H.order()}, |E(H_1)| = {H.size()}; "
|
||||
f"3-edge-coloured.")
|
||||
|
||||
# ---- STEP 2: initialise the protected set
|
||||
E = {
|
||||
frozenset(spike),
|
||||
frozenset(side_0),
|
||||
frozenset(side_1),
|
||||
frozenset(merged),
|
||||
}
|
||||
if verbose:
|
||||
print(f"STEP 2: |E_protected| = {len(E)}")
|
||||
|
||||
# ---- STEP 3-4: iterate
|
||||
for t in range(2, max_iterations + 1):
|
||||
face_data = find_safe_pentagonal_face(H, E)
|
||||
if face_data is None:
|
||||
if verbose:
|
||||
print(f"\nTerminated at iteration t = {t}: "
|
||||
f"no pentagonal face avoids E.")
|
||||
break
|
||||
boundary, externals, A = face_data
|
||||
f_vec = [coloring[e] for e in externals]
|
||||
choices = valid_indices(f_vec)
|
||||
if not choices:
|
||||
print(f" iter t = {t}: f-vector {f_vec} has no valid index "
|
||||
f"(Lemma 2.4 should preclude this --- bug!)")
|
||||
break
|
||||
i_t = choices[0]
|
||||
v_n_label = ('v_n', t)
|
||||
H, coloring, named = apply_reduction(
|
||||
H, boundary, externals, A, coloring, i_t, v_n_label)
|
||||
assert is_proper_3_coloring(H, coloring)
|
||||
E |= set(named.values())
|
||||
if verbose:
|
||||
print(f" iter t = {t:>2}: f = {f_vec}, chose i_t = {i_t}; "
|
||||
f"|V| = {H.order():>2}, |E_graph| = {H.size():>2}, "
|
||||
f"|E_protected| = {len(E):>2}")
|
||||
else:
|
||||
if verbose:
|
||||
print(f"\nReached max_iterations = {max_iterations}.")
|
||||
|
||||
if verbose:
|
||||
print(f"\nFinal H: |V| = {H.order()}, |E| = {H.size()}, "
|
||||
f"|E_protected| = {len(E)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_algorithm()
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Reduced dual: construction and verification.
|
||||
|
||||
Test input is the icosahedron G (the unique 5-regular triangulation, n=12).
|
||||
Its dual G' is the dodecahedron (a cubic plane graph, 20 vertices). We pick a
|
||||
degree-5 vertex v of G -- equivalently a pentagonal face F_v of G' -- and apply
|
||||
the reduced-dual construction of Definition 2.1:
|
||||
|
||||
1. delete the 5 dual vertices on the boundary of F_v (and incident edges),
|
||||
leaving 5 degree-2 vertices on a new face F;
|
||||
2. order those 5 vertices clockwise around F as A_0..A_4;
|
||||
3. add a vertex v_n joined to A_i, A_{i+1}, A_{i+2};
|
||||
4. add an edge A_{i+3} A_{i+4}.
|
||||
|
||||
We verify the result is again a cubic plane graph, and report the triangulation
|
||||
it is the dual of (its face count = the primal vertex count), to see how the
|
||||
vertex count changes relative to n.
|
||||
|
||||
The dodecahedron is built directly in its concentric "Schlegel" layout with
|
||||
F_v the inner pentagon, so the figures (draw_reduced_dual_steps.py) are clean.
|
||||
"""
|
||||
import math
|
||||
import networkx as nx
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build G' = dodecahedron with concentric positions; F_v = inner pentagon.
|
||||
# Vertex families a (inner pentagon), b, c, d (outer pentagon), 5 each.
|
||||
# Angles increase *clockwise* (90 - 72*i deg) so index order is clockwise.
|
||||
# ---------------------------------------------------------------------------
|
||||
def build_dual():
|
||||
pos = {}
|
||||
R = {'a': 1.0, 'b': 2.2, 'c': 3.6, 'd': 4.8}
|
||||
for i in range(5):
|
||||
for fam in ('a', 'b'):
|
||||
th = math.radians(90 - 72 * i)
|
||||
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
|
||||
for fam in ('c', 'd'):
|
||||
th = math.radians(90 - 72 * i - 36) # offset half a step
|
||||
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
|
||||
|
||||
Gp = nx.Graph()
|
||||
Gp.add_nodes_from(pos)
|
||||
for i in range(5):
|
||||
Gp.add_edge(('a', i), ('a', (i + 1) % 5)) # inner pentagon
|
||||
Gp.add_edge(('a', i), ('b', i)) # spokes a-b
|
||||
Gp.add_edge(('b', i), ('c', i)) # b-c
|
||||
Gp.add_edge(('b', i), ('c', (i - 1) % 5)) # b-c (other side)
|
||||
Gp.add_edge(('c', i), ('d', i)) # spokes c-d
|
||||
Gp.add_edge(('d', i), ('d', (i + 1) % 5)) # outer pentagon
|
||||
|
||||
Fv_boundary = [('a', i) for i in range(5)] # inner pentagon
|
||||
return Gp, pos, Fv_boundary
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Face / dual helpers.
|
||||
# ---------------------------------------------------------------------------
|
||||
def faces_of(G):
|
||||
"""Return the list of faces (each a list of vertices) of a plane graph."""
|
||||
ok, emb = nx.check_planarity(G)
|
||||
assert ok, "graph is not planar"
|
||||
seen, faces = set(), []
|
||||
for u in emb:
|
||||
for v in emb[u]:
|
||||
if (u, v) not in seen:
|
||||
faces.append(emb.traverse_face(u, v, mark_half_edges=seen))
|
||||
return faces
|
||||
|
||||
|
||||
def dual_of(G):
|
||||
"""Combinatorial dual (all faces, including outer) of a plane graph."""
|
||||
faces = faces_of(G)
|
||||
edge_faces = {}
|
||||
for fi, face in enumerate(faces):
|
||||
for j in range(len(face)):
|
||||
e = frozenset((face[j], face[(j + 1) % len(face)]))
|
||||
edge_faces.setdefault(e, []).append(fi)
|
||||
D = nx.MultiGraph()
|
||||
D.add_nodes_from(range(len(faces)))
|
||||
for e, fs in edge_faces.items():
|
||||
if len(fs) == 2:
|
||||
D.add_edge(fs[0], fs[1])
|
||||
elif len(fs) == 1: # shouldn't happen for 2-connected G
|
||||
pass
|
||||
return D, faces
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The reduced-dual construction.
|
||||
# ---------------------------------------------------------------------------
|
||||
def clockwise_order(verts, pos):
|
||||
"""Order verts clockwise around their centroid, starting from the topmost."""
|
||||
cx = sum(pos[v][0] for v in verts) / len(verts)
|
||||
cy = sum(pos[v][1] for v in verts) / len(verts)
|
||||
ang = {v: math.atan2(pos[v][1] - cy, pos[v][0] - cx) for v in verts}
|
||||
ccw = sorted(verts, key=lambda v: ang[v]) # counterclockwise
|
||||
cw = list(reversed(ccw)) # clockwise
|
||||
start = max(range(len(cw)), key=lambda k: pos[cw[k]][1]) # topmost first
|
||||
return cw[start:] + cw[:start]
|
||||
|
||||
|
||||
def apply_reduction(Gp, pos, Fv_boundary, i=0):
|
||||
"""Apply Definition 2.1 and return a dict capturing each stage."""
|
||||
Ghat = Gp.copy()
|
||||
npos = dict(pos)
|
||||
|
||||
# (1) delete the 5 boundary dual vertices of F_v
|
||||
Ghat.remove_nodes_from(Fv_boundary)
|
||||
deg2 = [v for v in Ghat if Ghat.degree(v) == 2]
|
||||
assert len(deg2) == 5, f"expected 5 degree-2 vertices, got {len(deg2)}"
|
||||
|
||||
# (2) order them clockwise around the new face F
|
||||
A = clockwise_order(deg2, pos)
|
||||
|
||||
# (3) new vertex v_n joined to A_i, A_{i+1}, A_{i+2}
|
||||
apex_nbrs = [A[(i + k) % 5] for k in range(3)]
|
||||
ax = sum(npos[v][0] for v in apex_nbrs) / 3
|
||||
ay = sum(npos[v][1] for v in apex_nbrs) / 3
|
||||
v_n = 'v_n'
|
||||
npos[v_n] = (ax * 0.55, ay * 0.55) # pull toward the 3 nbrs
|
||||
Ghat.add_node(v_n)
|
||||
for u in apex_nbrs:
|
||||
Ghat.add_edge(v_n, u)
|
||||
|
||||
# (4) chord between the remaining two
|
||||
chord = (A[(i + 3) % 5], A[(i + 4) % 5])
|
||||
Ghat.add_edge(*chord)
|
||||
|
||||
return {
|
||||
'Ghat': Ghat, 'pos': npos, 'A': A, 'v_n': v_n,
|
||||
'apex_nbrs': apex_nbrs, 'chord': chord,
|
||||
'deleted': list(Fv_boundary),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
Gp, pos, Fv = build_dual()
|
||||
|
||||
# --- verify G' is the dodecahedron = dual of the icosahedron ---
|
||||
assert nx.check_planarity(Gp)[0]
|
||||
assert all(d == 3 for _, d in Gp.degree()), "G' not cubic"
|
||||
assert nx.is_isomorphic(Gp, nx.dodecahedral_graph()), "G' is not dodecahedron"
|
||||
Dico, _ = dual_of(Gp)
|
||||
Dico = nx.Graph(Dico)
|
||||
print(f"G (icosahedron) : dual of G' has {Dico.number_of_nodes()} vertices, "
|
||||
f"degrees {sorted({d for _, d in Dico.degree()})}")
|
||||
print(f"G' (dodecahedron): {Gp.number_of_nodes()} vertices, "
|
||||
f"{Gp.number_of_edges()} edges, "
|
||||
f"{len(faces_of(Gp))} faces; cubic={all(d==3 for _,d in Gp.degree())}")
|
||||
|
||||
# --- apply the reduced-dual construction ---
|
||||
res = apply_reduction(Gp, pos, Fv, i=0)
|
||||
Ghat = res['Ghat']
|
||||
cubic = all(d == 3 for _, d in Ghat.degree())
|
||||
planar = nx.check_planarity(Ghat)[0]
|
||||
ghat_simple = (nx.number_of_selfloops(Ghat) == 0) # Graph: no parallels
|
||||
nfaces = len(faces_of(Ghat))
|
||||
print()
|
||||
print(f"reduced dual G^_v,i : {Ghat.number_of_nodes()} vertices, "
|
||||
f"{Ghat.number_of_edges()} edges, {nfaces} faces")
|
||||
print(f" cubic : {cubic}")
|
||||
print(f" planar : {planar}")
|
||||
print(f" simple : {ghat_simple}")
|
||||
|
||||
# --- the triangulation it is dual to ---
|
||||
Dred_multi, _ = dual_of(Ghat)
|
||||
Dred = nx.Graph(Dred_multi)
|
||||
dred_simple = (Dred.number_of_edges() == Dred_multi.number_of_edges())
|
||||
is_tri = all(len(f) == 3 for f in faces_of(Dred)) if planar else None
|
||||
print()
|
||||
print(f"dual of reduced dual : {Dred.number_of_nodes()} vertices "
|
||||
f"(= faces of G^), degree seq "
|
||||
f"{sorted((d for _, d in Dred.degree()), reverse=True)}")
|
||||
print(f" is a triangulation : {is_tri}")
|
||||
print(f" simple : {dred_simple}")
|
||||
|
||||
n = Dico.number_of_nodes()
|
||||
print()
|
||||
print(f"VERTEX COUNT: G has n = {n}; reduced triangulation has "
|
||||
f"{Dred.number_of_nodes()} (change = "
|
||||
f"{Dred.number_of_nodes() - n}).")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,380 @@
|
||||
"""Search for a counterexample to Conjecture 3.6.
|
||||
|
||||
For each min-deg-5 triangulation G and each reduced dual H_1, find a proper
|
||||
3-edge-coloring phi_1 of H_1 that satisfies (i) chord-apex (spike = merged)
|
||||
and (ii) both Kempe-chain conditions of Lemma 2.7 -- i.e., the kind of
|
||||
coloring that a minimal counterexample's reduced dual would be forced to
|
||||
have. Run Algorithm 3.1 to completion from phi_1, get the final colouring
|
||||
phi_t* on H_t*. Then enumerate all proper 3-edge-colorings of H_t*, find the
|
||||
Kempe class containing phi_t*, and check whether any coloring in that class
|
||||
is "all-distinct" (every spike_t != merged_t for t = 1, ..., t*).
|
||||
|
||||
If no such all-distinct coloring exists in phi_t*'s Kempe class:
|
||||
counterexample to Conjecture 3.6 found.
|
||||
|
||||
Run with: sage experiments/search_conj_3_6_counterexample.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 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, 'boundary': boundary,
|
||||
'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 edge_idx(edges, e_frozen):
|
||||
for i, e in enumerate(edges):
|
||||
if frozenset((e[0], e[1])) == e_frozen:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
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 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 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, coloring_dict, named_step1, vn_start=1000):
|
||||
"""Returns (H_final, coloring_final, list_of_named_dicts)."""
|
||||
H = H.copy()
|
||||
coloring = dict(coloring_dict)
|
||||
all_named = [named_step1]
|
||||
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]
|
||||
named = {
|
||||
'spike': frozenset(spike),
|
||||
'side_0': frozenset(side_0),
|
||||
'side_1': frozenset(side_1),
|
||||
'merged': frozenset(merged),
|
||||
}
|
||||
E |= set(named.values())
|
||||
all_named.append(named)
|
||||
return H, coloring, all_named
|
||||
|
||||
|
||||
def kempe_class_via_union_find(edges, colorings):
|
||||
"""For each coloring, attempt every Kempe swap and union. Returns parent
|
||||
array of disjoint-set forest."""
|
||||
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])
|
||||
return [find(i) for i in range(n)]
|
||||
|
||||
|
||||
def is_all_distinct(edges, col, all_named):
|
||||
for named in all_named:
|
||||
i_spike = edge_idx(edges, named['spike'])
|
||||
i_merged = edge_idx(edges, named['merged'])
|
||||
if i_spike is None or i_merged is None:
|
||||
return False
|
||||
if col[i_spike] == col[i_merged]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_one_triangulation(G, n, idx_label, time_budget):
|
||||
G_planar = G.is_planar(set_embedding=True)
|
||||
if not G_planar:
|
||||
return None
|
||||
D = dual_of(G)
|
||||
D.is_planar(set_embedding=True)
|
||||
for face in D.faces():
|
||||
if len(face) != 5:
|
||||
continue
|
||||
for i_red in range(5):
|
||||
res = apply_reduction(D, face, i_red, 999)
|
||||
if res is None:
|
||||
continue
|
||||
H_1, named_1 = res['H'], res['named']
|
||||
edges_1, colorings_1 = proper_3_edge_colorings(H_1)
|
||||
candidates = [col for col in colorings_1
|
||||
if matches_chord_apex_kempe(edges_1, col, named_1)]
|
||||
if not candidates:
|
||||
continue
|
||||
# For the first candidate, run algorithm and check.
|
||||
phi_1 = candidates[0]
|
||||
coloring_dict = {frozenset((e[0], e[1])): c
|
||||
for e, c in zip(edges_1, phi_1)}
|
||||
H_final, phi_final_dict, all_named = run_algorithm_to_completion(
|
||||
H_1, coloring_dict, named_1)
|
||||
edges_final, colorings_final = proper_3_edge_colorings(H_final)
|
||||
# find phi_final as a tuple
|
||||
phi_final = tuple(
|
||||
phi_final_dict[frozenset((e[0], e[1]))] for e in edges_final
|
||||
)
|
||||
if phi_final not in colorings_final:
|
||||
# not in enumeration, skip
|
||||
continue
|
||||
print(f" n={n}, tri#{idx_label}, face_size={len(face)}, "
|
||||
f"i_red={i_red}: H_1 has {len(candidates)} chord+Kempe "
|
||||
f"colorings; algorithm produced H_t* with |V|={H_final.order()}, "
|
||||
f"|E|={H_final.size()}; H_t* has {len(colorings_final)} "
|
||||
f"proper colorings.")
|
||||
# Kempe classes
|
||||
parent = kempe_class_via_union_find(edges_final, colorings_final)
|
||||
phi_final_idx = colorings_final.index(phi_final)
|
||||
phi_class = parent[phi_final_idx]
|
||||
n_in_class = sum(1 for p in parent if p == phi_class)
|
||||
print(f" phi_final's Kempe class has {n_in_class} colorings; "
|
||||
f"total classes: {len(set(parent))}")
|
||||
# search for all-distinct in same class
|
||||
for ci, p in enumerate(parent):
|
||||
if p != phi_class:
|
||||
continue
|
||||
if is_all_distinct(edges_final, colorings_final[ci], all_named):
|
||||
print(f" found all-distinct coloring in same Kempe "
|
||||
f"class -- Conj 3.6 holds for this instance.")
|
||||
break
|
||||
else:
|
||||
print(f" *** NO all-distinct coloring in phi_final's "
|
||||
f"Kempe class -- CONJECTURE 3.6 FAILS HERE ***")
|
||||
return {
|
||||
'n': n, 'tri': idx_label, 'face_size': len(face),
|
||||
'i_red': i_red, 'H_final': H_final,
|
||||
'phi_final': phi_final, 'all_named': all_named,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def main(max_n=16, time_budget_sec=600):
|
||||
start = time.time()
|
||||
for n in range(12, max_n + 1):
|
||||
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) ===")
|
||||
try:
|
||||
it = graphs.triangulations(n, minimum_degree=5)
|
||||
except Exception as ex:
|
||||
print(f" cannot enumerate: {ex}")
|
||||
continue
|
||||
for idx, G in enumerate(it, start=1):
|
||||
if time.time() - start > time_budget_sec:
|
||||
print(f"Time budget exhausted mid-n={n}.")
|
||||
return
|
||||
hit = check_one_triangulation(G, n, idx,
|
||||
time_budget_sec - (time.time() - start))
|
||||
if hit is not None:
|
||||
print()
|
||||
print(f"=== Counterexample to Conjecture 3.6 ===")
|
||||
print(f" n = {hit['n']}, triangulation #{hit['tri']}, "
|
||||
f"i_red = {hit['i_red']}")
|
||||
print(f" H_t* has |V|={hit['H_final'].order()}, "
|
||||
f"|E|={hit['H_final'].size()}")
|
||||
return
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,225 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user