dual_decomposition: Conj 3.6 (face/Kempe witness) and constructive lift

Paper:
- Lemmas 3.4 (exactly one match) and 3.5 (all-distinct exists for 4-colourable
  G) replace the earlier conjecture; both have proofs.
- Add Conjecture 3.6: every proper 3-edge-colouring of a counterexample's
  reduced dual has a face with two same-colour edges that share a Kempe
  cycle with the merged edge, neither of them being the merged edge.

Experiments (all under experiments/):
- search_conj_3_6_counterexample.py: finds n=14 tri#1 i_red=0 where the
  algorithm's phi_t* sits in a Kempe class with no all-distinct colouring
  (disproves an earlier formulation).
- check_kempe_class.py / check_kempe_class_invariance.py /
  check_kempe_class_monotone.py: Kempe-class counts on H_1 and H_t* for
  small triangulations; neither monotonicity direction holds.
- check_all_distinct_exists.py: even in the conj-3.6 disproof case, H_t*
  itself admits all-distinct colourings in the *other* Kempe class.
- check_constrained_feasibility.py: literal H_t*-interpretation of
  C1 + K0 + K1 is empirically unsatisfiable (gap in proof strategy noted).
- check_conj_face_kempe.py / check_conj_face_kempe_n15.py: test Conj 3.6
  on chord-apex+Kempe colourings of reduced duals at n=12, 14, 15;
  216/216 colourings on n=14 satisfy the conjecture, others vacuous.
- draw_step1_conj36.py: figure showing a Conj 3.6 witness on H_1 with two
  new vertices on the witness edges and a new red bridge between them.
- draw_step1_conj36_recolored.py: same but with the Kempe cycle recoloured
  alternately from merged so propriety holds.
- draw_lift_to_Gprime.py: lifts the modified+recoloured H_1 back to a
  proper 3-edge-colouring of the modified G' (24+2 vertices, 39 edges,
  same Tutte layout as figure 3's first graphic so positions line up).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 11:27:50 -04:00
parent 03dcd7c2fa
commit 464c524fa1
20 changed files with 3326 additions and 113 deletions
@@ -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,192 @@
"""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 has two same-color edges (e1, e2)
and both, together with merged, lie on a Kempe cycle kc. Else None."""
merged_idx = edge_idx(edges, named['merged'])
c_merged = col[merged_idx]
# All Kempe cycles through merged (one per color pair (c_merged, c'))
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)
for i in range(len(face_edge_indices)):
for j in range(i + 1, len(face_edge_indices)):
e1, e2 = face_edge_indices[i], face_edge_indices[j]
if col[e1] != col[e2]: 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/dual_decomposition_minimal_counterexamples/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,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,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,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,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,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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

@@ -17,12 +17,14 @@
\newlabel{alg:iterated-reduction}{{3.1}{7}} \newlabel{alg:iterated-reduction}{{3.1}{7}}
\newlabel{rem:alg-invariants}{{3.2}{7}} \newlabel{rem:alg-invariants}{{3.2}{7}}
\newlabel{rem:alg-chord-apex}{{3.3}{7}} \newlabel{rem:alg-chord-apex}{{3.3}{7}}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Algorithm\nonbreakingspace 3.1\hbox {} on $G'=\mathrm {dual}(G)$, where $G$ is the first min-degree-$5$ plantri triangulation on $14$ vertices and $\varphi _1$ is a specific proper $3$-edge-colouring of $H_1$ that satisfies both the chord-apex condition (Lemma\nonbreakingspace 2.6\hbox {}) and the Kempe-cycle condition (Lemma\nonbreakingspace 2.7\hbox {}), found by \texttt {experiments/search\_kempe\_property.py}. \emph {Left:} $G'$ ($24$ vertices, $36$ edges) with the chosen pentagonal face shaded. \emph {Centre:} $H_1$ ($20$ vertices, $30$ edges) after step\nonbreakingspace (1) with $i_1 = 1$, $3$-edge-coloured by $\varphi _1$; the four edges around $v_n^{(1)}$ in $E$ are drawn thicker, and the spike and merged edges share the colour green. \emph {Right:} $H_2$ ($16$ vertices, $24$ edges) after step\nonbreakingspace (3) with $i_t = 3$; eight edges are protected, and the algorithm terminates one step later (no remaining safe pentagonal face in $H_2$). The generating script is \texttt {experiments/draw\_iterated\_reduction\_n14.py}; layouts are Tutte barycentric embeddings with the outer face picked to keep $v_n^{(1)}, v_n^{(2)}$ in the interior.}}{8}{}\protected@file@percent }
\newlabel{fig:iterated-reduction-trace}{{3}{8}}
\newlabel{lem:exactly-one-match}{{3.4}{8}}
\newlabel{tocindent-1}{0pt} \newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{0pt} \newlabel{tocindent0}{0pt}
\newlabel{tocindent1}{17.77782pt} \newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt} \newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt} \newlabel{tocindent3}{0pt}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Algorithm\nonbreakingspace 3.1\hbox {} on $G'=\mathrm {dual}(G)$, where $G$ is the first min-degree-$5$ plantri triangulation on $14$ vertices and $\varphi _1$ is a specific proper $3$-edge-colouring of $H_1$ that satisfies both the chord-apex condition (Lemma\nonbreakingspace 2.6\hbox {}) and the Kempe-cycle condition (Lemma\nonbreakingspace 2.7\hbox {}), found by \texttt {experiments/search\_kempe\_property.py}. \emph {Left:} $G'$ ($24$ vertices, $36$ edges) with the chosen pentagonal face shaded. \emph {Centre:} $H_1$ ($20$ vertices, $30$ edges) after step\nonbreakingspace (1) with $i_1 = 1$, $3$-edge-coloured by $\varphi _1$; the four edges around $v_n^{(1)}$ in $E$ are drawn thicker, and the spike and merged edges share the colour green. \emph {Right:} $H_2$ ($16$ vertices, $24$ edges) after step\nonbreakingspace (3) with $i_t = 3$; eight edges are protected, and the algorithm terminates one step later (no remaining safe pentagonal face in $H_2$). The generating script is \texttt {experiments/draw\_iterated\_reduction\_n14.py}; layouts are Tutte barycentric embeddings with the outer face picked to keep $v_n^{(1)}, v_n^{(2)}$ in the interior.}}{8}{}\protected@file@percent } \newlabel{lem:all-distinct-exists}{{3.5}{9}}
\newlabel{fig:iterated-reduction-trace}{{3}{8}} \newlabel{conj:face-monochromatic-pair-on-merged-kempe-cycle}{{3.6}{9}}
\newlabel{conj:no-all-distinct-coloring}{{3.4}{8}} \gdef \@abspage@last{9}
\gdef \@abspage@last{8}
@@ -1,6 +1,6 @@
# Fdb version 3 # Fdb version 3
["pdflatex"] 1779490854 "/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" "paper.pdf" "paper" 1779490855 ["pdflatex"] 1779636334 "/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" "paper.pdf" "paper" 1779636335
"/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" 1779490854 8400 319c3b8b7a4962c7285398f53dc0c37a "" "/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex" 1779636334 29173 3355ad986dd2045d7c83ee9e1f4197ba ""
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 "" "/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 ""
@@ -19,17 +19,21 @@
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1136768653 1116 933a60c408fc0a863a92debe84b2d294 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1136768653 1116 933a60c408fc0a863a92debe84b2d294 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti7.tfm" 1136768653 1492 86331993fe614793f5e7e755835c31c5 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm" 1136768653 768 1321e9409b4137d6fb428ac9dc956269 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm" 1136768653 768 1321e9409b4137d6fb428ac9dc956269 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb" 1248133631 30251 6afa5cb1d0204815a708a080681d4674 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb" 1248133631 30251 6afa5cb1d0204815a708a080681d4674 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1248133631 36299 5f9df58c2139e7edcf37c8fca4bd384d "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1248133631 36299 5f9df58c2139e7edcf37c8fca4bd384d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb" 1248133631 37912 77d683123f92148345f3fc36a38d9ab1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb" 1248133631 36281 c355509802a035cadc5f15869451dcee "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb" 1248133631 36281 c355509802a035cadc5f15869451dcee ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.pfb" 1248133631 31809 8670ca339bf94e56da1fc21c80635e2a ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1248133631 32762 224316ccc9ad3ca0423a14971cfa7fc1 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1248133631 32762 224316ccc9ad3ca0423a14971cfa7fc1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1248133631 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1248133631 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1248133631 32569 5e5ddc8df908dea60932f3c484a54c0d "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1248133631 32569 5e5ddc8df908dea60932f3c484a54c0d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb" 1248133631 32915 7bf7720c61a5b3a7ff25b0964421c9b6 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 "" "/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 ""
@@ -59,12 +63,18 @@
"/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 "" "/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 ""
"/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc "" "/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc ""
"/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c "" "/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c ""
"fig_alg_step0.png" 1779555533 79063 a44949fedf2f79b9d1af80dda627bad0 ""
"fig_alg_step1.png" 1779555533 81138 4655a02d13b723226d9e88c98f25fc14 ""
"fig_alg_step2.png" 1779555533 78532 6a2ff0be6a6e40110e6632afdb232782 ""
"fig_chord_apex_step1.png" 1779497048 128193 2156b95264c4e7be78a47a8f38e36347 ""
"fig_chord_apex_step2.png" 1779497048 103549 502d6b9497ab67090f34da927b068fdc ""
"fig_chord_apex_step3.png" 1779497048 104380 d3d0e553ec0e4ca7ef3f4de590f42ccb ""
"fig_reduced_dual_step1.png" 1779490218 117795 4da7754ac28df9e809cfa1069e081c53 "" "fig_reduced_dual_step1.png" 1779490218 117795 4da7754ac28df9e809cfa1069e081c53 ""
"fig_reduced_dual_step2.png" 1779490218 96839 4f94c996220a2758dd0ff21ebdb9b2be "" "fig_reduced_dual_step2.png" 1779490218 96839 4f94c996220a2758dd0ff21ebdb9b2be ""
"fig_reduced_dual_step3.png" 1779490218 102877 d2a5db5532697441e3150e2ad26b9173 "" "fig_reduced_dual_step3.png" 1779490218 102877 d2a5db5532697441e3150e2ad26b9173 ""
"fig_reduced_dual_step4.png" 1779490218 107439 b30c7a68e32660f9bd3a8070bdb96944 "" "fig_reduced_dual_step4.png" 1779490218 107439 b30c7a68e32660f9bd3a8070bdb96944 ""
"paper.aux" 1779490855 1250 5e537a38e8b733147def35d69e964063 "pdflatex" "paper.aux" 1779636335 3620 34f7ba6436aaccd7664676641a126dbe "pdflatex"
"paper.tex" 1779490854 8400 319c3b8b7a4962c7285398f53dc0c37a "" "paper.tex" 1779636334 29173 3355ad986dd2045d7c83ee9e1f4197ba ""
(generated) (generated)
"paper.aux" "paper.aux"
"paper.log" "paper.log"
@@ -247,17 +247,52 @@ INPUT ./fig_reduced_dual_step4.png
INPUT fig_reduced_dual_step4.png INPUT fig_reduced_dual_step4.png
INPUT ./fig_reduced_dual_step4.png INPUT ./fig_reduced_dual_step4.png
INPUT ./fig_reduced_dual_step4.png INPUT ./fig_reduced_dual_step4.png
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti7.tfm
INPUT ./fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step1.png
INPUT fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step1.png
INPUT ./fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step2.png
INPUT fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step2.png
INPUT ./fig_chord_apex_step3.png
INPUT ./fig_chord_apex_step3.png
INPUT fig_chord_apex_step3.png
INPUT ./fig_chord_apex_step3.png
INPUT ./fig_chord_apex_step3.png
INPUT ./fig_alg_step0.png
INPUT ./fig_alg_step0.png
INPUT fig_alg_step0.png
INPUT ./fig_alg_step0.png
INPUT ./fig_alg_step0.png
INPUT ./fig_alg_step1.png
INPUT ./fig_alg_step1.png
INPUT fig_alg_step1.png
INPUT ./fig_alg_step1.png
INPUT ./fig_alg_step1.png
INPUT ./fig_alg_step2.png
INPUT ./fig_alg_step2.png
INPUT fig_alg_step2.png
INPUT ./fig_alg_step2.png
INPUT ./fig_alg_step2.png
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmtt10.tfm
INPUT paper.aux INPUT paper.aux
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb
@@ -1,12 +1,12 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 23 MAY 2026 13:19 This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 24 MAY 2026 11:25
entering extended mode entering extended mode
restricted \write18 enabled. restricted \write18 enabled.
file:line:error style messages enabled.
%&-line parsing enabled. %&-line parsing enabled.
**paper.tex **/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex
(./paper.tex (/Users/didericis/Code/math-research/papers/dual_decomposition_minimal_counterexamples/paper.tex
LaTeX2e <2021-11-15> patch level 1 LaTeX2e <2021-11-15> patch level 1
L3 programming layer <2022-02-24> L3 programming layer <2022-02-24> (/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
Document Class: amsart 2020/05/29 v2.20.6 Document Class: amsart 2020/05/29 v2.20.6
\linespacing=\dimen138 \linespacing=\dimen138
\normalparindent=\dimen139 \normalparindent=\dimen139
@@ -18,17 +18,14 @@ Package: amsmath 2021/10/15 v2.17l AMS math features
For additional information on amsmath, use the `?' option. For additional information on amsmath, use the `?' option.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty (/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text Package: amstext 2021/08/26 v2.01 AMS text
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks16 \@emptytoks=\toks16
\ex@=\dimen140 \ex@=\dimen140
)) )) (/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen141 \pmbraise@=\dimen141
) ) (/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names Package: amsopn 2021/08/26 v2.02 operator names
) )
\inf@bad=\count185 \inf@bad=\count185
@@ -69,13 +66,10 @@ LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
LaTeX Info: Redefining \[ on input line 2938. LaTeX Info: Redefining \[ on input line 2938.
LaTeX Info: Redefining \] on input line 2939. LaTeX Info: Redefining \] on input line 2939.
) )
LaTeX Font Info: Trying to load font information for U+msa on input line 397 LaTeX Font Info: Trying to load font information for U+msa on input line 397.
. (/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A File: umsa.fd 2013/01/14 v3.01 AMS symbols A
) ) (/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4 \symAMSa=\mathgroup4
\symAMSb=\mathgroup5 \symAMSb=\mathgroup5
@@ -106,42 +100,33 @@ LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
\thm@postskip=\skip55 \thm@postskip=\skip55
\thm@headsep=\skip56 \thm@headsep=\skip56
\dth@everypar=\toks26 \dth@everypar=\toks26
) ) (/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols Package: amssymb 2013/01/14 v3.01 AMS font symbols
) ) (/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR) Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2014/10/28 v1.15 key=value parser (DPC) Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
\KV@toks@=\toks27 \KV@toks@=\toks27
) ) (/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR) Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC) Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
) ) (/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
) )
Package graphics Info: Driver file: pdftex.def on input line 107. Package graphics Info: Driver file: pdftex.def on input line 107.
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
)) ))
\Gin@req@height=\dimen150 \Gin@req@height=\dimen150
\Gin@req@width=\dimen151 \Gin@req@width=\dimen151
) )
\c@theorem=\count272 \c@theorem=\count272
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX) File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count273 \l__color_backend_stack_int=\count273
\l__pdf_internal_box=\box53 \l__pdf_internal_box=\box53
) ) (./paper.aux)
(./paper.aux)
\openout1 = `paper.aux'. \openout1 = `paper.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 27. LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 27.
@@ -159,17 +144,13 @@ LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 27. LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27. LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Trying to load font information for U+msa on input line 27. LaTeX Font Info: Trying to load font information for U+msa on input line 27.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd (/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A File: umsa.fd 2013/01/14 v3.01 AMS symbols A
) )
LaTeX Font Info: Trying to load font information for U+msb on input line 27. LaTeX Font Info: Trying to load font information for U+msb on input line 27.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B File: umsb.fd 2013/01/14 v3.01 AMS symbols B
) ) (/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).] [Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count274 \scratchcounter=\count274
\scratchdimen=\dimen152 \scratchdimen=\dimen152
@@ -184,18 +165,12 @@ File: umsb.fd 2013/01/14 v3.01 AMS symbols B
\everyMPtoPDFconversion=\toks29 \everyMPtoPDFconversion=\toks29
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty ) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4 Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 485.
85. (/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Live
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg )) [1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
Overfull \hbox (41.917pt too wide) in paragraph at lines 145--147 Overfull \hbox (41.917pt too wide) in paragraph at lines 145--147
[]\OT1/cmr/m/n/10 List the five degree-$2$ ver-tices in clock-wise or-der aroun []\OT1/cmr/m/n/10 List the five degree-$2$ ver-tices in clock-wise or-der around $\OML/cmm/m/it/10 F$ \OT1/cmr/m/n/10 as $\OML/cmm/m/it/10 A \OT1/cmr/m/n/10 = (\OML/cmm/m/it/10 A[]; A[]; A[]; A[]; A[]\OT1/cmr/m/n/10 )$.
d $\OML/cmm/m/it/10 F$ \OT1/cmr/m/n/10 as $\OML/cmm/m/it/10 A \OT1/cmr/m/n/10 =
(\OML/cmm/m/it/10 A[]; A[]; A[]; A[]; A[]\OT1/cmr/m/n/10 )$.
[] []
<fig_reduced_dual_step1.png, id=17, 517.79329pt x 499.08812pt> <fig_reduced_dual_step1.png, id=17, 517.79329pt x 499.08812pt>
@@ -221,8 +196,7 @@ Package pdftex.def Info: fig_reduced_dual_step4.png used on input line 168.
LaTeX Warning: `h' float specifier changed to `ht'. LaTeX Warning: `h' float specifier changed to `ht'.
[2] [3 <./fig_reduced_dual_step1.png> <./fig_reduced_dual_step2.png> <./fig_red [2] [3 <./fig_reduced_dual_step1.png> <./fig_reduced_dual_step2.png> <./fig_reduced_dual_step3.png> <./fig_reduced_dual_step4.png>]
uced_dual_step3.png> <./fig_reduced_dual_step4.png>]
<fig_chord_apex_step1.png, id=34, 505.03976pt x 502.06393pt> <fig_chord_apex_step1.png, id=34, 505.03976pt x 502.06393pt>
File: fig_chord_apex_step1.png Graphic file (type png) File: fig_chord_apex_step1.png Graphic file (type png)
<use fig_chord_apex_step1.png> <use fig_chord_apex_step1.png>
@@ -242,15 +216,9 @@ Package pdftex.def Info: fig_chord_apex_step3.png used on input line 291.
LaTeX Warning: `h' float specifier changed to `ht'. LaTeX Warning: `h' float specifier changed to `ht'.
[4] [5 <./fig_chord_apex_step1.png> <./fig_chord_apex_step2.png> <./fig_chord_a [4] [5 <./fig_chord_apex_step1.png> <./fig_chord_apex_step2.png> <./fig_chord_apex_step3.png>] [6]
pex_step3.png>] [6]
Overfull \hbox (4.76643pt too wide) in paragraph at lines 433--440 Overfull \hbox (4.76643pt too wide) in paragraph at lines 433--440
\OT1/cmr/m/n/10 which $\OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[ \OT1/cmr/m/n/10 which $\OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/cmr/m/n/10 ) = \OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/cmr/m/n/10 )$ and $\OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/cmr/m/n/10 )\OML/cmm/m/it/10 ; '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/cmr/m/n/10 )\OML/cmm/m/it/10 ; '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/cmr/m/n/10 )$
]\OT1/cmr/m/n/10 ) = \OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\
OT1/cmr/m/n/10 )$ and $\OML/cmm/m/it/10 '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[
]\OT1/cmr/m/n/10 )\OML/cmm/m/it/10 ; '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\O
T1/cmr/m/n/10 )\OML/cmm/m/it/10 ; '[]\OT1/cmr/m/n/10 (\OML/cmm/m/it/10 f[]\OT1/
cmr/m/n/10 )$
[] []
<fig_alg_step0.png, id=49, 399.6106pt x 459.55217pt> <fig_alg_step0.png, id=49, 399.6106pt x 459.55217pt>
@@ -270,21 +238,17 @@ Package pdftex.def Info: fig_alg_step2.png used on input line 481.
(pdftex.def) Requested size: 115.20264pt x 132.48134pt. (pdftex.def) Requested size: 115.20264pt x 132.48134pt.
Underfull \hbox (badness 4391) in paragraph at lines 498--498 Underfull \hbox (badness 4391) in paragraph at lines 498--498
\OT1/cmr/m/sc/10 Figure 3.\OT1/cmr/m/n/10 Algorithm 3.1[] on $\OML/cmm/m/it/10 \OT1/cmr/m/sc/10 Figure 3.\OT1/cmr/m/n/10 Algorithm 3.1[] on $\OML/cmm/m/it/10 G[] \OT1/cmr/m/n/10 = [](\OML/cmm/m/it/10 G\OT1/cmr/m/n/10 )$, where $\OML/cmm/m/it/10 G$
G[] \OT1/cmr/m/n/10 = [](\OML/cmm/m/it/10 G\OT1/cmr/m/n/10 )$, where $\OML/cmm/
m/it/10 G$
[] []
Underfull \hbox (badness 3623) in paragraph at lines 498--498 Underfull \hbox (badness 3623) in paragraph at lines 498--498
\OT1/cmr/m/n/10 is the first min-degree-$5$ plantri tri-an-gu-la-tion on $14$ v \OT1/cmr/m/n/10 is the first min-degree-$5$ plantri tri-an-gu-la-tion on $14$ ver-
er-
[] []
Underfull \hbox (badness 3179) in paragraph at lines 498--498 Underfull \hbox (badness 3179) in paragraph at lines 498--498
\OT1/cmr/m/n/10 tices and $\OML/cmm/m/it/10 '[]$ \OT1/cmr/m/n/10 is a spe-cific \OT1/cmr/m/n/10 tices and $\OML/cmm/m/it/10 '[]$ \OT1/cmr/m/n/10 is a spe-cific proper $3$-edge-colouring of $\OML/cmm/m/it/10 H[]$
proper $3$-edge-colouring of $\OML/cmm/m/it/10 H[]$
[] []
@@ -297,37 +261,20 @@ Underfull \hbox (badness 6094) in paragraph at lines 498--498
\OT1/cmr/m/n/10 and the Kempe-cycle con-di-tion (Lemma 2.7[]), found by \OT1/cmr/m/n/10 and the Kempe-cycle con-di-tion (Lemma 2.7[]), found by
[] []
[7] [8 <./fig_alg_step0.png> <./fig_alg_step1.png> <./fig_alg_step2.png>] [7] [8 <./fig_alg_step0.png> <./fig_alg_step1.png> <./fig_alg_step2.png>] [9] (./paper.aux) )
(./paper.aux) )
Here is how much of TeX's memory you used: Here is how much of TeX's memory you used:
3079 strings out of 478268 3082 strings out of 478268
43848 string characters out of 5846347 44173 string characters out of 5846347
344259 words of memory out of 5000000 344279 words of memory out of 5000000
21116 multiletter control sequences out of 15000+600000 21118 multiletter control sequences out of 15000+600000
476532 words of font info for 56 fonts, out of 8000000 for 9000 476532 words of font info for 56 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191 1302 hyphenation exceptions out of 8191
69i,8n,76p,1306b,298s stack positions out of 10000i,1000n,20000p,200000b,200000s 69i,8n,76p,1392b,298s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/ </usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c Output written on paper.pdf (9 pages, 977618 bytes).
m/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm
/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/
cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cm
mi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr1
0.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.p
fb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb>
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb></u
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb></us
r/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb></usr/
local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr/lo
cal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/loc
al/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local
/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt10.pfb></usr/local/
texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (8 pages, 969703 bytes).
PDF statistics: PDF statistics:
137 PDF objects out of 1000 (max. 8388607) 140 PDF objects out of 1000 (max. 8388607)
72 compressed objects within 1 object stream 74 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000) 0 named destinations out of 1000 (max. 500000)
51 words of extra memory for PDF output out of 10000 (max. 10000000) 51 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -499,16 +499,121 @@ v_n^{(2)}$ in the interior.}
\label{fig:iterated-reduction-trace} \label{fig:iterated-reduction-trace}
\end{figure} \end{figure}
\begin{lemma}[Exactly one matching pair in the algorithm's output]
\label{lem:exactly-one-match}
Let $G$ be a minimal counterexample to the Four Colour Theorem, and let
$(H_{t^*}, \varphi_{t^*})$ be the final graph-and-colouring produced by some
terminating execution of Algorithm~\ref{alg:iterated-reduction} on $G$, with
named pairs $(\mathrm{spike}_t, \mathrm{merged}_t)$ for $t = 1, \dots, t^*$.
Then there is exactly one $t$ with
$\varphi_{t^*}(\mathrm{spike}_t) = \varphi_{t^*}(\mathrm{merged}_t)$, and it
is $t = 1$.
\end{lemma}
\begin{proof}
The algorithm never re-colours an existing edge: at each iteration
step~(3d) every surviving edge keeps its $\varphi_{t-1}$-colour, and the
four new edges receive fresh colours forced by propriety. Hence for every
$1 \leq k \leq t \leq t^*$,
\[
\varphi_t(\mathrm{spike}_k) = \varphi_k(\mathrm{spike}_k),
\qquad
\varphi_t(\mathrm{merged}_k) = \varphi_k(\mathrm{merged}_k);
\]
the colours of the step-$k$ named edges, once written, are permanent. It
suffices to compare $\varphi_k(\mathrm{spike}_k)$ and
$\varphi_k(\mathrm{merged}_k)$ at the step where each pair is introduced.
\textbf{Case $k = 1$.} Since $G$ is a minimal counterexample, $H_1$ is a
reduced dual of $G'$. Lemma~\ref{lem:chord-apex} applied to $\varphi_1$ gives
$\varphi_1(\mathrm{spike}_1) = \varphi_1(\mathrm{merged}_1)$.
\textbf{Case $k \geq 2$.} At step $k$ the algorithm picks an index $i_k$ for
which $f_{i_k+3} = f_{i_k+4}$ (chord consistency) and $f_{i_k}, f_{i_k+1},
f_{i_k+2}$ are pairwise distinct (propriety at the new $v_n$), where $f$ is
the external vector of the chosen pentagonal face of $H_{k-1}$ under
$\varphi_{k-1}$. Step~(3d) then assigns
\[
\varphi_k(\mathrm{spike}_k) = f_{i_k+1},
\qquad
\varphi_k(\mathrm{merged}_k) = f_{i_k+3}.
\]
By Lemma~\ref{lem:pentagonal-externals}, $f$ has the $(2,2,1)$ pattern: a
block of three consecutive positions $\{p, p+1, p+2\}$ (mod $5$) on which it
is constantly some colour $c$, while the remaining two positions
$\{p+3, p+4\}$ hold the two non-$c$ colours, one each. The condition
$f_{i_k+3} = f_{i_k+4}$ forces $(i_k+3, i_k+4)$ to be either $(p, p+1)$ or
$(p+1, p+2)$ --- the two consecutive pairs inside the block --- and
correspondingly $i_k + 1 \in \{p+3, p+4\}$, \emph{outside} the block. So
$f_{i_k+1}$ is not $c$, whereas $f_{i_k+3} = c$, and hence
$\varphi_k(\mathrm{spike}_k) \neq \varphi_k(\mathrm{merged}_k)$.
Combining the two cases, exactly one $t \in \{1, \dots, t^*\}$ --- namely
$t = 1$ --- has $\varphi_{t^*}(\mathrm{spike}_t) =
\varphi_{t^*}(\mathrm{merged}_t)$.
\end{proof}
\begin{lemma}[All-distinct colouring exists on a 4-colourable graph]
\label{lem:all-distinct-exists}
Let $G$ be a $4$-colourable maximal planar graph of minimum degree $\geq 5$
(equivalently, a maximal planar graph that is \emph{not} a minimal
counterexample to the Four Colour Theorem). Then there is an execution of
Algorithm~\ref{alg:iterated-reduction} on $G$ whose final colouring
$\varphi_{t^*}$ satisfies
$\varphi_{t^*}(\mathrm{spike}_t) \neq \varphi_{t^*}(\mathrm{merged}_t)$ for
every $t \in \{1, \dots, t^*\}$. In particular, there exists a proper
$3$-edge-colouring of $H_{t^*}$ under which every spike-merged pair has
distinct colours.
\end{lemma}
\begin{proof}
The argument mirrors Lemma~\ref{lem:exactly-one-match}, but extends a
colouring \emph{downward} from $G'$ rather than carrying one forward from
$H_1$.
Since $G$ is $4$-colourable, by Tait's theorem
$G' = \mathrm{dual}(G)$ admits a proper $3$-edge-colouring $\xi$. Apply
Lemma~\ref{lem:pentagonal-externals} to $\xi$ at the pentagonal face $F_v$
selected in step~(1): the external vector $f = (f_0, \dots, f_4)$ at $F_v$
under $\xi$ has the $(3,1,1)$ cyclic-consecutive shape, with a block of
three consecutive positions $\{p, p+1, p+2\}$ (mod $5$) holding a common
colour $c$, and the remaining two positions $\{p+3, p+4\}$ holding the two
non-$c$ colours, one each. The algorithm's choice of $i_1$ forces
$\{i_1+3, i_1+4\}$ inside the $c$-block (so the chord is consistently
coloured) and the three positions $\{i_1, i_1+1, i_1+2\}$ pairwise distinct;
in particular $i_1+1$ lies \emph{outside} the $c$-block.
Choose $\varphi_1$ to be the proper $3$-edge-colouring of $H_1$ that agrees
with $\xi$ on every surviving edge and assigns each new edge at $A_j$ the
unique third colour at $A_j$. Then
$\varphi_1(\mathrm{spike}_1) = f_{i_1+1}$, a value not equal to $c$, while
$\varphi_1(\mathrm{merged}_1) = f_{i_1+3} = c$, so
$\varphi_1(\mathrm{spike}_1) \neq \varphi_1(\mathrm{merged}_1)$.
The same argument repeats at every step $k \geq 2$: the external vector at
the chosen pentagonal face under $\varphi_{k-1}$ has the $(3,1,1)$
cyclic-consecutive shape (Lemma~\ref{lem:pentagonal-externals}), the
algorithm's index choice $i_k$ puts $i_k+3, i_k+4$ inside the colour block
and $i_k+1$ outside, and step~(3d) thus assigns
$\varphi_k(\mathrm{spike}_k) \neq \varphi_k(\mathrm{merged}_k)$. The
algorithm preserves these colours through every later step, so
$\varphi_{t^*}(\mathrm{spike}_t) \neq \varphi_{t^*}(\mathrm{merged}_t)$ for
every $t \in \{1, \dots, t^*\}$.
\end{proof}
\begin{conjecture} \begin{conjecture}
\label{conj:no-all-distinct-coloring} \label{conj:face-monochromatic-pair-on-merged-kempe-cycle}
Let $G$ be a maximal planar graph of minimum degree $\geq 5$, and let Let $G$ be a minimal counterexample to the Four Colour Theorem, and let
$H_{t^*}$ be the final reduced graph produced by some terminating execution $\widehat{G}'_{v,i}$ be a reduced dual of $G' = \mathrm{dual}(G)$. Then for
of Algorithm~\ref{alg:iterated-reduction} on $G$, with the corresponding every proper $3$-edge-colouring $\varphi$ of $\widehat{G}'_{v,i}$ there exist
sequence of named pairs $(\mathrm{spike}_t, \mathrm{merged}_t)$ for a face $F$ of $\widehat{G}'_{v,i}$ and two distinct edges
$t = 1, \dots, t^*$. Then $G$ is a minimal counterexample to the Four Colour $e_1, e_2 \in \partial F$, with neither $e_1$ nor $e_2$ equal to the merged
Theorem if and only if there is no proper $3$-edge-colouring of $H_{t^*}$ edge, such that
under which $\mathrm{spike}_t$ and $\mathrm{merged}_t$ receive different \begin{enumerate}
colours for every $t \in \{1, \dots, t^*\}$. \item $\varphi(e_1) = \varphi(e_2)$, and
\item $e_1$, $e_2$, and the merged edge all lie on a common
$\{a, b\}$-Kempe cycle of $\varphi$.
\end{enumerate}
\end{conjecture} \end{conjecture}
\end{document} \end{document}