dual_decomposition: iterated-reduction algorithm + Kempe/chord-apex search
- Add section 3 with Algorithm 3.1 (iterated reduction with protected edges) and remarks on invariants and chord-apex applicability. - Add fig:iterated-reduction-trace illustrating the algorithm on G' = dodecahedron (G' -> H_1 -> H_2 -> terminate). - experiments/iterated_reduction.py: Sage implementation of the algorithm. - experiments/draw_iterated_reduction.py: produces the 3 trace figures. - experiments/check_dodecahedron_kempe.py: enumerate proper 3-edge-colorings of the dodecahedron's reduced dual and check the chord-apex + Kempe-cycle conditions (0 of 36 colorings satisfy all three). - experiments/search_kempe_property.py: search across min-deg-5 triangulations; the n = 14 first plantri triangulation is the smallest hit (reduced dual has 20 v, 30 e). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
"""Check whether the dodecahedron's reduced dual admits a proper
|
||||
3-edge-colouring with:
|
||||
(i) colour(spike) == colour(merged), AND
|
||||
(ii) the {c_spike, c_side0}-Kempe cycle through the spike contains both
|
||||
side-0 and the merged edge, AND
|
||||
(iii) the {c_spike, c_side1}-Kempe cycle through the spike contains both
|
||||
side-1 and the merged edge.
|
||||
|
||||
Definitions follow Algorithm 3.1 in paper.tex with G = icosahedron, G' =
|
||||
dodecahedron, i_1 = 0; the reduced dual has 16 vertices and 24 edges.
|
||||
|
||||
Lemmas 2.6 and 2.7 force these properties for ANY proper 3-edge-colouring
|
||||
of the reduced dual of a *minimal counterexample's* dual. The dodecahedron
|
||||
is the dual of the icosahedron --- which IS 4-colourable, so the lemmas do
|
||||
not apply and the question is non-trivial.
|
||||
"""
|
||||
from sage.all import Graph
|
||||
|
||||
|
||||
def build_reduced_dodecahedron(i_red=0):
|
||||
edges = []
|
||||
for k in range(5):
|
||||
edges.append((('b', k), ('c', k)))
|
||||
edges.append((('b', k), ('c', (k - 1) % 5)))
|
||||
edges.append((('c', k), ('d', k)))
|
||||
edges.append((('d', k), ('d', (k + 1) % 5)))
|
||||
v_n = 'v_n'
|
||||
edges.append((v_n, ('b', i_red % 5)))
|
||||
edges.append((v_n, ('b', (i_red + 1) % 5)))
|
||||
edges.append((v_n, ('b', (i_red + 2) % 5)))
|
||||
edges.append((('b', (i_red + 3) % 5), ('b', (i_red + 4) % 5)))
|
||||
return Graph(edges, multiedges=False, loops=False)
|
||||
|
||||
|
||||
def enumerate_proper_3_edge_colorings(G):
|
||||
"""Return (edge_list, list_of_colorings) where each coloring is a list of
|
||||
3-edge-colours indexed by edge_list."""
|
||||
edges = list(G.edges(labels=False))
|
||||
n = len(edges)
|
||||
adj = [[] for _ in range(n)]
|
||||
for i in range(n):
|
||||
u, v = edges[i][0], edges[i][1]
|
||||
for j in range(i):
|
||||
x, y = edges[j][0], edges[j][1]
|
||||
if u in (x, y) or v in (x, y):
|
||||
adj[i].append(j)
|
||||
adj[j].append(i)
|
||||
coloring = [-1] * n
|
||||
results = []
|
||||
|
||||
def back(k):
|
||||
if k == n:
|
||||
results.append(coloring[:])
|
||||
return
|
||||
for c in range(3):
|
||||
if all(coloring[j] != c for j in adj[k]):
|
||||
coloring[k] = c
|
||||
back(k + 1)
|
||||
coloring[k] = -1
|
||||
|
||||
back(0)
|
||||
return edges, results
|
||||
|
||||
|
||||
def kempe_cycle_edges(edges, coloring, start_edge_idx, color_pair):
|
||||
"""Return the set of edge-indices in the Kempe cycle containing
|
||||
edges[start_edge_idx] in the subgraph of edges with colour in
|
||||
color_pair."""
|
||||
a, b = color_pair
|
||||
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
|
||||
if start_edge_idx not in in_sub:
|
||||
return None
|
||||
visited = {start_edge_idx}
|
||||
stack = [start_edge_idx]
|
||||
while stack:
|
||||
cur = stack.pop()
|
||||
u, v = edges[cur][0], edges[cur][1]
|
||||
for j in in_sub:
|
||||
if j in visited:
|
||||
continue
|
||||
x, y = edges[j][0], edges[j][1]
|
||||
if u in (x, y) or v in (x, y):
|
||||
visited.add(j)
|
||||
stack.append(j)
|
||||
return visited
|
||||
|
||||
|
||||
def main():
|
||||
G = build_reduced_dodecahedron(i_red=0)
|
||||
print(f"|V| = {G.order()}, |E| = {G.size()}")
|
||||
|
||||
edges, colorings = enumerate_proper_3_edge_colorings(G)
|
||||
print(f"Proper 3-edge-colorings total: {len(colorings)}")
|
||||
|
||||
v_n = 'v_n'
|
||||
spike_set = frozenset({v_n, ('b', 1)})
|
||||
side_0_set = frozenset({v_n, ('b', 0)})
|
||||
side_1_set = frozenset({v_n, ('b', 2)})
|
||||
merged_set = frozenset({('b', 3), ('b', 4)})
|
||||
|
||||
idx = {}
|
||||
for i, e in enumerate(edges):
|
||||
s = frozenset((e[0], e[1]))
|
||||
if s == spike_set: idx['spike'] = i
|
||||
if s == side_0_set: idx['side_0'] = i
|
||||
if s == side_1_set: idx['side_1'] = i
|
||||
if s == merged_set: idx['merged'] = i
|
||||
|
||||
n_chord_apex = 0 # spike == merged
|
||||
n_kc0_ok = 0 # condition (ii)
|
||||
n_kc1_ok = 0 # condition (iii)
|
||||
n_all_ok = 0 # all three
|
||||
example = None
|
||||
|
||||
for col in colorings:
|
||||
c_spike = col[idx['spike']]
|
||||
c_merged = col[idx['merged']]
|
||||
c_s0 = col[idx['side_0']]
|
||||
c_s1 = col[idx['side_1']]
|
||||
if c_spike != c_merged:
|
||||
continue
|
||||
n_chord_apex += 1
|
||||
|
||||
kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
|
||||
kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
|
||||
|
||||
kc0_ok = kc0 is not None and idx['side_0'] in kc0 and idx['merged'] in kc0
|
||||
kc1_ok = kc1 is not None and idx['side_1'] in kc1 and idx['merged'] in kc1
|
||||
n_kc0_ok += int(kc0_ok)
|
||||
n_kc1_ok += int(kc1_ok)
|
||||
if kc0_ok and kc1_ok:
|
||||
n_all_ok += 1
|
||||
if example is None:
|
||||
example = (col, kc0, kc1)
|
||||
|
||||
print()
|
||||
print(f"Colorings with spike == merged (chord-apex condition): {n_chord_apex}")
|
||||
print(f" ... + {{c, c_0}}-Kempe cycle through spike/side-0/merged: {n_kc0_ok}")
|
||||
print(f" ... + {{c, c_1}}-Kempe cycle through spike/side-1/merged: {n_kc1_ok}")
|
||||
print(f" ... ALL THREE conditions: {n_all_ok}")
|
||||
|
||||
if example is not None:
|
||||
col, kc0, kc1 = example
|
||||
c_spike = col[idx['spike']]
|
||||
print()
|
||||
print("Example coloring (one of {} matching):".format(n_all_ok))
|
||||
for role in ('spike', 'side_0', 'side_1', 'merged'):
|
||||
print(f" {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
|
||||
print(f" spike colour = merged colour = {c_spike}")
|
||||
print(f" Kempe cycle {{c, c_0}} (through spike): {len(kc0)} edges")
|
||||
for i in sorted(kc0):
|
||||
print(f" [c={col[i]}] {edges[i]}")
|
||||
print(f" Kempe cycle {{c, c_1}} (through spike): {len(kc1)} edges")
|
||||
for i in sorted(kc1):
|
||||
print(f" [c={col[i]}] {edges[i]}")
|
||||
else:
|
||||
# Dissect a sample chord-apex coloring that fails the Kempe condition
|
||||
print()
|
||||
print("Sample chord-apex coloring (fails the Kempe-chain condition):")
|
||||
for col in colorings:
|
||||
if col[idx['spike']] != col[idx['merged']]:
|
||||
continue
|
||||
c_spike = col[idx['spike']]
|
||||
c_s0 = col[idx['side_0']]
|
||||
c_s1 = col[idx['side_1']]
|
||||
for role in ('spike', 'side_0', 'side_1', 'merged'):
|
||||
print(f" {role:7s}: colour {col[idx[role]]}, edge {tuple(edges[idx[role]])}")
|
||||
kc0 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s0))
|
||||
kc1 = kempe_cycle_edges(edges, col, idx['spike'], (c_spike, c_s1))
|
||||
kc_m0 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s0))
|
||||
kc_m1 = kempe_cycle_edges(edges, col, idx['merged'], (c_spike, c_s1))
|
||||
print(f" spike's {{c, c_0}}-Kempe cycle has {len(kc0)} edges; "
|
||||
f"side_0 in it = {idx['side_0'] in kc0}, "
|
||||
f"merged in it = {idx['merged'] in kc0}")
|
||||
print(f" merged's {{c, c_0}}-Kempe cycle has {len(kc_m0)} edges; "
|
||||
f"disjoint from spike's? {kc0.isdisjoint(kc_m0)}")
|
||||
print(f" spike's {{c, c_1}}-Kempe cycle has {len(kc1)} edges; "
|
||||
f"side_1 in it = {idx['side_1'] in kc1}, "
|
||||
f"merged in it = {idx['merged'] in kc1}")
|
||||
print(f" merged's {{c, c_1}}-Kempe cycle has {len(kc_m1)} edges; "
|
||||
f"disjoint from spike's? {kc1.isdisjoint(kc_m1)}")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
"""Draw the iterated reduction algorithm's trace on the dodecahedron.
|
||||
|
||||
Produces three figures:
|
||||
fig_alg_step0.png -- G' (dodecahedron) with F_v (inner pentagon) shaded.
|
||||
fig_alg_step1.png -- H_1 (post step 1), 3-edge-coloured; 4 protected edges.
|
||||
fig_alg_step2.png -- H_2 (post step 2), 3-edge-coloured; 8 protected edges;
|
||||
algorithm terminates.
|
||||
|
||||
Run with: sage experiments/draw_iterated_reduction.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_coloring import edge_coloring
|
||||
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'] # proper-edge-colour palette
|
||||
GRAY = '#9ca3af'
|
||||
DARK = '#374151'
|
||||
HIGHLIGHT = '#fef3c7'
|
||||
|
||||
|
||||
def dodecahedron_positions():
|
||||
pos = {}
|
||||
R = {'a': 1.0, 'b': 2.2, 'c': 3.6, 'd': 4.8}
|
||||
for i in range(5):
|
||||
for fam in ('a', 'b'):
|
||||
th = math.radians(90 - 72 * i)
|
||||
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
|
||||
for fam in ('c', 'd'):
|
||||
th = math.radians(90 - 72 * i - 36)
|
||||
pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th))
|
||||
return pos
|
||||
|
||||
|
||||
def build_dodecahedron():
|
||||
edges = []
|
||||
for i in range(5):
|
||||
edges.append((('a', i), ('a', (i + 1) % 5)))
|
||||
edges.append((('a', i), ('b', i)))
|
||||
edges.append((('b', i), ('c', i)))
|
||||
edges.append((('b', i), ('c', (i - 1) % 5)))
|
||||
edges.append((('c', i), ('d', i)))
|
||||
edges.append((('d', i), ('d', (i + 1) % 5)))
|
||||
G = Graph(edges, multiedges=False, loops=False)
|
||||
G.is_planar(set_embedding=True)
|
||||
return G
|
||||
|
||||
|
||||
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 = []
|
||||
for B_k in boundary:
|
||||
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
||||
if len(outer) != 1:
|
||||
break
|
||||
externals.append(frozenset([B_k, outer[0]]))
|
||||
A.append(outer[0])
|
||||
else:
|
||||
if not any(e in protected for e in boundary_edges + externals):
|
||||
return boundary, externals, A
|
||||
return None
|
||||
|
||||
|
||||
def valid_indices(f_vec):
|
||||
out = []
|
||||
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:
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
|
||||
def draw(ax, G, pos, *, coloring=None, protected=None,
|
||||
shade_face=None):
|
||||
if shade_face:
|
||||
poly = [pos[v] for v in shade_face]
|
||||
ax.add_patch(Polygon(poly, closed=True, facecolor=HIGHLIGHT,
|
||||
edgecolor='none', zorder=0))
|
||||
protected = protected or set()
|
||||
for u, v in G.edges(labels=False):
|
||||
e = frozenset([u, v])
|
||||
c = C[coloring[e]] if coloring is not None else GRAY
|
||||
lw = 3.8 if e in 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 G.vertices(sort=False):
|
||||
x, y = pos[v]
|
||||
if isinstance(v, tuple) and v[0] == 'v_n':
|
||||
t = v[1]
|
||||
ax.scatter(x, y, s=320, color=HIGHLIGHT, marker='s',
|
||||
edgecolors='black', linewidths=1.2, zorder=4)
|
||||
ax.annotate(f'$v_n^{{({t})}}$', (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)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
|
||||
|
||||
def main():
|
||||
G = build_dodecahedron()
|
||||
pos = dodecahedron_positions()
|
||||
F_v = [('a', i) for i in range(5)]
|
||||
|
||||
# ----- Step 0: G' with F_v shaded -----
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
draw(ax, G, pos, shade_face=F_v)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step0.png'),
|
||||
dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
# ----- Step 1: Definition 2.1 at F_v with i_1 = 0 -----
|
||||
safe = find_safe_pentagonal_face(G, set())
|
||||
boundary_1, externals_1, A_1 = safe
|
||||
G1 = G.copy()
|
||||
for v in boundary_1:
|
||||
G1.delete_vertex(v)
|
||||
v_n_1 = ('v_n', 1)
|
||||
G1.add_vertex(v_n_1)
|
||||
G1.add_edge(v_n_1, A_1[0])
|
||||
G1.add_edge(v_n_1, A_1[1])
|
||||
G1.add_edge(v_n_1, A_1[2])
|
||||
G1.add_edge(A_1[3], A_1[4])
|
||||
G1.is_planar(set_embedding=True)
|
||||
pos1 = {v: p for v, p in pos.items() if v not in boundary_1}
|
||||
cx = (pos[A_1[0]][0] + pos[A_1[1]][0] + pos[A_1[2]][0]) / 3
|
||||
cy = (pos[A_1[0]][1] + pos[A_1[1]][1] + pos[A_1[2]][1]) / 3
|
||||
pos1[v_n_1] = (cx * 0.55, cy * 0.55)
|
||||
|
||||
cols = edge_coloring(G1, value_only=False)
|
||||
coloring = {}
|
||||
for k, edge_list in enumerate(cols):
|
||||
for u, v in edge_list:
|
||||
coloring[frozenset([u, v])] = k
|
||||
|
||||
E = {
|
||||
frozenset([v_n_1, A_1[1]]), # spike
|
||||
frozenset([v_n_1, A_1[0]]), # side-0
|
||||
frozenset([v_n_1, A_1[2]]), # side-1
|
||||
frozenset([A_1[3], A_1[4]]), # merged
|
||||
}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
draw(ax, G1, pos1, coloring=coloring, protected=E)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step1.png'),
|
||||
dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
# ----- Step 2: reduce at the only remaining safe face (outer pentagon) -----
|
||||
safe = find_safe_pentagonal_face(G1, E)
|
||||
if safe is None:
|
||||
print("ERROR: expected an outer pentagonal face but none found.")
|
||||
return
|
||||
boundary_2, externals_2, A_2 = safe
|
||||
f_vec = [coloring[e] for e in externals_2]
|
||||
choices = valid_indices(f_vec)
|
||||
if not choices:
|
||||
print(f"ERROR: f-vector {f_vec} has no valid index.")
|
||||
return
|
||||
i_t = choices[0]
|
||||
|
||||
G2 = G1.copy()
|
||||
for v in boundary_2:
|
||||
G2.delete_vertex(v)
|
||||
v_n_2 = ('v_n', 2)
|
||||
G2.add_vertex(v_n_2)
|
||||
G2.add_edge(v_n_2, A_2[i_t])
|
||||
G2.add_edge(v_n_2, A_2[(i_t + 1) % 5])
|
||||
G2.add_edge(v_n_2, A_2[(i_t + 2) % 5])
|
||||
G2.add_edge(A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5])
|
||||
G2.is_planar(set_embedding=True)
|
||||
|
||||
coloring2 = {e: c for e, c in coloring.items()
|
||||
if not any(u in boundary_2 for u in e)}
|
||||
side_0_2 = frozenset([v_n_2, A_2[i_t]])
|
||||
spike_2 = frozenset([v_n_2, A_2[(i_t + 1) % 5]])
|
||||
side_1_2 = frozenset([v_n_2, A_2[(i_t + 2) % 5]])
|
||||
merged_2 = frozenset([A_2[(i_t + 3) % 5], A_2[(i_t + 4) % 5]])
|
||||
coloring2[side_0_2] = coloring[externals_2[i_t]]
|
||||
coloring2[spike_2] = coloring[externals_2[(i_t + 1) % 5]]
|
||||
coloring2[side_1_2] = coloring[externals_2[(i_t + 2) % 5]]
|
||||
coloring2[merged_2] = coloring[externals_2[(i_t + 3) % 5]]
|
||||
|
||||
pos2 = {v: p for v, p in pos1.items() if v not in boundary_2}
|
||||
nbrs = [A_2[i_t], A_2[(i_t + 1) % 5], A_2[(i_t + 2) % 5]]
|
||||
cx = sum(pos2[a][0] for a in nbrs) / 3
|
||||
cy = sum(pos2[a][1] for a in nbrs) / 3
|
||||
r = math.hypot(cx, cy)
|
||||
# v_n^{(2)} lies outside the surviving graph (the deleted d's were outermost)
|
||||
target_r = 5.0
|
||||
pos2[v_n_2] = (cx * target_r / r, cy * target_r / r)
|
||||
|
||||
E |= {side_0_2, spike_2, side_1_2, merged_2}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
draw(ax, G2, pos2, coloring=coloring2, protected=E)
|
||||
fig.savefig(os.path.join(OUT_DIR, 'fig_alg_step2.png'),
|
||||
dpi=170, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
print(f"Wrote fig_alg_step{{0,1,2}}.png to {OUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Iterated reduced-dual reduction with protected edges (Sage).
|
||||
|
||||
Implements Algorithm 3.1 from paper.tex:
|
||||
|
||||
0. G' = dual of G (a triangulation conjectured to be a minimal
|
||||
counterexample).
|
||||
1. Apply Definition 2.1 to G' at a pentagonal face to get H_1; find a
|
||||
proper 3-edge-colouring phi_1 of H_1.
|
||||
2. Initialise protected edge set E := {spike, side-0, side-1, merged} from
|
||||
step 1.
|
||||
3. Iterate: find a pentagonal face of H_{t-1} whose 10 incident edges are
|
||||
disjoint from E, pick a valid index i_t (Lemma 2.4), apply
|
||||
Definition 2.1, extend phi_{t-1} to phi_t, and add the four new named
|
||||
edges to E.
|
||||
4. Terminate when no safe pentagonal face exists.
|
||||
|
||||
We run on the icosahedron-dodecahedron pair as a concrete trace; the
|
||||
icosahedron is 4-colourable, so the dodecahedron is 3-edge-colourable and
|
||||
the algorithm terminates combinatorially.
|
||||
|
||||
Run with:
|
||||
sage experiments/iterated_reduction.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_coloring import edge_coloring
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build G' = dodecahedron with the same naming as experiments/reduced_dual.py:
|
||||
# vertex families a (inner pentagon, will play the role of partial F_v's
|
||||
# boundary vertices for the first reduction), b, c, d.
|
||||
# ---------------------------------------------------------------------------
|
||||
def build_dodecahedron():
|
||||
edges = []
|
||||
for i in range(5):
|
||||
edges.append((('a', i), ('a', (i + 1) % 5))) # inner pentagon
|
||||
edges.append((('a', i), ('b', i))) # spoke a-b
|
||||
edges.append((('b', i), ('c', i))) # b-c
|
||||
edges.append((('b', i), ('c', (i - 1) % 5))) # b-c (other side)
|
||||
edges.append((('c', i), ('d', i))) # spoke c-d
|
||||
edges.append((('d', i), ('d', (i + 1) % 5))) # outer pentagon
|
||||
G = Graph(edges, multiedges=False, loops=False)
|
||||
assert G.is_planar(set_embedding=True), "dodecahedron not planar?"
|
||||
return G
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3-edge-colour a cubic plane graph; return None if not 3-edge-colourable.
|
||||
# ---------------------------------------------------------------------------
|
||||
def proper_3_coloring(G):
|
||||
classes = edge_coloring(G, value_only=False)
|
||||
if len(classes) > 3:
|
||||
return None
|
||||
out = {}
|
||||
for k, edge_list in enumerate(classes):
|
||||
for u, v in edge_list:
|
||||
out[frozenset([u, v])] = k
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Face search: pentagonal face whose 10 incident edges are outside `protected`.
|
||||
# Returns (boundary, externals, A) or None.
|
||||
# ---------------------------------------------------------------------------
|
||||
def find_safe_pentagonal_face(G, protected):
|
||||
for face in G.faces():
|
||||
if len(face) != 5:
|
||||
continue
|
||||
boundary = [u for (u, v) in face]
|
||||
boundary_edges = [frozenset([u, v]) for (u, v) in face]
|
||||
# the external edge at each boundary vertex (the one not on the face)
|
||||
externals = []
|
||||
A = []
|
||||
for B_k in boundary:
|
||||
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
||||
if len(outer) != 1:
|
||||
# In a cubic graph each face-boundary vertex has exactly one
|
||||
# external edge; otherwise something is off.
|
||||
break
|
||||
externals.append(frozenset([B_k, outer[0]]))
|
||||
A.append(outer[0])
|
||||
else:
|
||||
if not any(e in protected for e in boundary_edges + externals):
|
||||
return boundary, externals, A
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lemma 2.4: any proper 3-edge-colouring forces the external vector at a
|
||||
# pentagonal face to have shape (a, b, c, c, c) up to cyclic rotation. The
|
||||
# valid index i for the reduction is one where positions i+3, i+4 host the
|
||||
# doubled colour and positions i, i+1, i+2 host three distinct colours.
|
||||
# ---------------------------------------------------------------------------
|
||||
def valid_indices(f_vec):
|
||||
out = []
|
||||
for i in range(5):
|
||||
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
|
||||
continue
|
||||
triple = {f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}
|
||||
if len(triple) == 3:
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apply Definition 2.1 at (F, i): delete the 5 boundary vertices, add v_n
|
||||
# connected to A[i], A[i+1], A[i+2], add the chord A[i+3]-A[i+4]. Extend the
|
||||
# colouring: every surviving edge keeps its colour; each new edge takes the
|
||||
# colour of the deleted external at the same A_k (the unique third colour at
|
||||
# A_k under the surviving edges).
|
||||
# ---------------------------------------------------------------------------
|
||||
def apply_reduction(G, boundary, externals, A, coloring, i, v_n_label):
|
||||
G_new = G.copy()
|
||||
for v in boundary:
|
||||
G_new.delete_vertex(v)
|
||||
G_new.add_vertex(v_n_label)
|
||||
side_0 = (v_n_label, A[i])
|
||||
spike = (v_n_label, A[(i + 1) % 5])
|
||||
side_1 = (v_n_label, A[(i + 2) % 5])
|
||||
merged = (A[(i + 3) % 5], A[(i + 4) % 5])
|
||||
G_new.add_edges([side_0, spike, side_1, merged])
|
||||
assert G_new.is_planar(set_embedding=True), "reduction broke planarity"
|
||||
|
||||
coloring_new = {
|
||||
e: c for e, c in coloring.items() if not any(u in boundary for u in e)
|
||||
}
|
||||
coloring_new[frozenset(side_0)] = coloring[externals[i]]
|
||||
coloring_new[frozenset(spike)] = coloring[externals[(i + 1) % 5]]
|
||||
coloring_new[frozenset(side_1)] = coloring[externals[(i + 2) % 5]]
|
||||
coloring_new[frozenset(merged)] = coloring[externals[(i + 3) % 5]]
|
||||
|
||||
named = {
|
||||
'spike': frozenset(spike),
|
||||
'side_0': frozenset(side_0),
|
||||
'side_1': frozenset(side_1),
|
||||
'merged': frozenset(merged),
|
||||
}
|
||||
return G_new, coloring_new, named
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sanity check: verify that a coloring is proper (each vertex sees 3 colours).
|
||||
# ---------------------------------------------------------------------------
|
||||
def is_proper_3_coloring(G, coloring):
|
||||
for v in G.vertex_iterator():
|
||||
seen = set()
|
||||
for u in G.neighbor_iterator(v):
|
||||
seen.add(coloring[frozenset([u, v])])
|
||||
if len(seen) != G.degree(v):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main driver.
|
||||
# ---------------------------------------------------------------------------
|
||||
def run_algorithm(max_iterations=50, verbose=True):
|
||||
if verbose:
|
||||
print("STEP 0: G' = dodecahedron (dual of the icosahedron)")
|
||||
G_prime = build_dodecahedron()
|
||||
if verbose:
|
||||
print(f" |V(G')| = {G_prime.order()}, |E(G')| = {G_prime.size()}")
|
||||
|
||||
# ---- STEP 1: apply Definition 2.1 to G' at the inner pentagon, i_1 = 0
|
||||
face_data = find_safe_pentagonal_face(G_prime, set())
|
||||
if face_data is None:
|
||||
print(" No pentagonal face in G'.")
|
||||
return
|
||||
boundary, externals, A = face_data
|
||||
|
||||
H = G_prime.copy()
|
||||
for v in boundary:
|
||||
H.delete_vertex(v)
|
||||
v_n_label = ('v_n', 1)
|
||||
H.add_vertex(v_n_label)
|
||||
side_0 = (v_n_label, A[0])
|
||||
spike = (v_n_label, A[1])
|
||||
side_1 = (v_n_label, A[2])
|
||||
merged = (A[3], A[4])
|
||||
H.add_edges([side_0, spike, side_1, merged])
|
||||
assert H.is_planar(set_embedding=True)
|
||||
|
||||
coloring = proper_3_coloring(H)
|
||||
if coloring is None:
|
||||
print(" H_1 is not 3-edge-colourable; algorithm halts.")
|
||||
return
|
||||
assert is_proper_3_coloring(H, coloring)
|
||||
if verbose:
|
||||
print(f"STEP 1: H_1 = reduced dual at the first face, i_1 = 0")
|
||||
print(f" |V(H_1)| = {H.order()}, |E(H_1)| = {H.size()}; "
|
||||
f"3-edge-coloured.")
|
||||
|
||||
# ---- STEP 2: initialise the protected set
|
||||
E = {
|
||||
frozenset(spike),
|
||||
frozenset(side_0),
|
||||
frozenset(side_1),
|
||||
frozenset(merged),
|
||||
}
|
||||
if verbose:
|
||||
print(f"STEP 2: |E_protected| = {len(E)}")
|
||||
|
||||
# ---- STEP 3-4: iterate
|
||||
for t in range(2, max_iterations + 1):
|
||||
face_data = find_safe_pentagonal_face(H, E)
|
||||
if face_data is None:
|
||||
if verbose:
|
||||
print(f"\nTerminated at iteration t = {t}: "
|
||||
f"no pentagonal face avoids E.")
|
||||
break
|
||||
boundary, externals, A = face_data
|
||||
f_vec = [coloring[e] for e in externals]
|
||||
choices = valid_indices(f_vec)
|
||||
if not choices:
|
||||
print(f" iter t = {t}: f-vector {f_vec} has no valid index "
|
||||
f"(Lemma 2.4 should preclude this --- bug!)")
|
||||
break
|
||||
i_t = choices[0]
|
||||
v_n_label = ('v_n', t)
|
||||
H, coloring, named = apply_reduction(
|
||||
H, boundary, externals, A, coloring, i_t, v_n_label)
|
||||
assert is_proper_3_coloring(H, coloring)
|
||||
E |= set(named.values())
|
||||
if verbose:
|
||||
print(f" iter t = {t:>2}: f = {f_vec}, chose i_t = {i_t}; "
|
||||
f"|V| = {H.order():>2}, |E_graph| = {H.size():>2}, "
|
||||
f"|E_protected| = {len(E):>2}")
|
||||
else:
|
||||
if verbose:
|
||||
print(f"\nReached max_iterations = {max_iterations}.")
|
||||
|
||||
if verbose:
|
||||
print(f"\nFinal H: |V| = {H.order()}, |E| = {H.size()}, "
|
||||
f"|E_protected| = {len(E)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_algorithm()
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
"""Search for a proper 3-edge-coloring of a reduced dual satisfying:
|
||||
|
||||
(i) color(spike) == color(merged)
|
||||
(ii) the {c_spike, c_side_0}-Kempe cycle through the spike contains
|
||||
side_0 AND merged
|
||||
(iii) the {c_spike, c_side_1}-Kempe cycle through the spike contains
|
||||
side_1 AND merged
|
||||
|
||||
Iterates over all min-degree-5 simple planar triangulations on n vertices
|
||||
(via plantri through Sage), then every pentagonal face of the dual G', then
|
||||
every index i in {0,...,4} for the reduced-dual construction, then every
|
||||
proper 3-edge-coloring of the resulting reduced dual. Stops on the first hit.
|
||||
|
||||
The icosahedron (n = 12) is the only n = 12 triangulation with min degree 5
|
||||
and was already known to fail (see check_dodecahedron_kempe.py).
|
||||
|
||||
Run with:
|
||||
sage experiments/search_kempe_property.py
|
||||
"""
|
||||
from sage.all import Graph
|
||||
from sage.graphs.graph_generators import graphs
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def dual_of(G):
|
||||
"""Combinatorial dual of a planar G with the embedding set on G."""
|
||||
faces = G.faces()
|
||||
edge_to_faces = {}
|
||||
for fi, face in enumerate(faces):
|
||||
for u, v in face:
|
||||
e = frozenset((u, v))
|
||||
edge_to_faces.setdefault(e, []).append(fi)
|
||||
dual_edges = []
|
||||
for e, fs in edge_to_faces.items():
|
||||
if len(fs) == 2:
|
||||
dual_edges.append((fs[0], fs[1]))
|
||||
return Graph(dual_edges, multiedges=False, loops=False)
|
||||
|
||||
|
||||
def apply_reduction(G, face, i):
|
||||
"""Apply Definition 2.1 at the pentagonal `face` (list of directed edges)
|
||||
with index i, returning (H, named) or (None, None) if the construction
|
||||
breaks."""
|
||||
boundary = [u for (u, v) in face]
|
||||
if len(set(boundary)) != 5:
|
||||
return None, None
|
||||
A = []
|
||||
for B_k in boundary:
|
||||
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
|
||||
if len(outer) != 1:
|
||||
return None, None
|
||||
A.append(outer[0])
|
||||
if len(set(A)) != 5:
|
||||
return None, None
|
||||
H = G.copy()
|
||||
for v in boundary:
|
||||
H.delete_vertex(v)
|
||||
v_n = '__v_n__'
|
||||
H.add_vertex(v_n)
|
||||
side_0 = (v_n, A[i % 5])
|
||||
spike = (v_n, A[(i + 1) % 5])
|
||||
side_1 = (v_n, A[(i + 2) % 5])
|
||||
merged = (A[(i + 3) % 5], A[(i + 4) % 5])
|
||||
# If merged would be a self-loop or duplicate of an existing edge, skip
|
||||
if A[(i + 3) % 5] == A[(i + 4) % 5]:
|
||||
return None, None
|
||||
H.add_edges([side_0, spike, side_1, merged])
|
||||
if H.has_multiple_edges() or H.has_loops():
|
||||
return None, None
|
||||
if not H.is_planar(set_embedding=True):
|
||||
return None, None
|
||||
# Cubic check
|
||||
if not all(H.degree(v) == 3 for v in H.vertex_iterator()):
|
||||
return None, None
|
||||
named = {
|
||||
'spike': frozenset(spike),
|
||||
'side_0': frozenset(side_0),
|
||||
'side_1': frozenset(side_1),
|
||||
'merged': frozenset(merged),
|
||||
}
|
||||
return H, named
|
||||
|
||||
|
||||
def proper_3_edge_colorings(G):
|
||||
edges = list(G.edges(labels=False))
|
||||
n_edges = len(edges)
|
||||
adj = [[] for _ in range(n_edges)]
|
||||
for i in range(n_edges):
|
||||
u, v = edges[i][0], edges[i][1]
|
||||
for j in range(i):
|
||||
x, y = edges[j][0], edges[j][1]
|
||||
if u in (x, y) or v in (x, y):
|
||||
adj[i].append(j)
|
||||
adj[j].append(i)
|
||||
coloring = [-1] * n_edges
|
||||
|
||||
def back(k):
|
||||
if k == n_edges:
|
||||
yield tuple(coloring)
|
||||
return
|
||||
for c in range(3):
|
||||
if all(coloring[j] != c for j in adj[k]):
|
||||
coloring[k] = c
|
||||
yield from back(k + 1)
|
||||
coloring[k] = -1
|
||||
|
||||
return edges, back(0)
|
||||
|
||||
|
||||
def kempe_cycle(edges, coloring, start_idx, color_pair):
|
||||
a, b = color_pair
|
||||
if coloring[start_idx] not in (a, b):
|
||||
return None
|
||||
in_sub = [i for i in range(len(edges)) if coloring[i] in (a, b)]
|
||||
visited = {start_idx}
|
||||
stack = [start_idx]
|
||||
while stack:
|
||||
cur = stack.pop()
|
||||
u, v = edges[cur][0], edges[cur][1]
|
||||
for j in in_sub:
|
||||
if j in visited:
|
||||
continue
|
||||
x, y = edges[j][0], edges[j][1]
|
||||
if u in (x, y) or v in (x, y):
|
||||
visited.add(j)
|
||||
stack.append(j)
|
||||
return visited
|
||||
|
||||
|
||||
def matches(edges, col, named_idx):
|
||||
c_spike = col[named_idx['spike']]
|
||||
c_merged = col[named_idx['merged']]
|
||||
if c_spike != c_merged:
|
||||
return False
|
||||
c_s0 = col[named_idx['side_0']]
|
||||
c_s1 = col[named_idx['side_1']]
|
||||
kc0 = kempe_cycle(edges, col, named_idx['spike'], (c_spike, c_s0))
|
||||
if named_idx['side_0'] not in kc0 or named_idx['merged'] not in kc0:
|
||||
return False
|
||||
kc1 = kempe_cycle(edges, col, named_idx['spike'], (c_spike, c_s1))
|
||||
if named_idx['side_1'] not in kc1 or named_idx['merged'] not in kc1:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def report(edges, col, named_idx, named, info):
|
||||
print()
|
||||
print("*** MATCH FOUND ***")
|
||||
for k, v in info.items():
|
||||
print(f" {k} = {v}")
|
||||
print("Named edges and their colours:")
|
||||
for role in ('spike', 'side_0', 'side_1', 'merged'):
|
||||
e = edges[named_idx[role]]
|
||||
print(f" {role:7s}: edge {tuple(e)}, colour {col[named_idx[role]]}")
|
||||
print(f"Full coloring (indexed by edge order):")
|
||||
for i, e in enumerate(edges):
|
||||
print(f" [{col[i]}] {tuple(e)}")
|
||||
|
||||
|
||||
def search(max_n=16, time_budget_sec=600):
|
||||
start = time.time()
|
||||
n = 12
|
||||
while n <= max_n:
|
||||
elapsed = time.time() - start
|
||||
if elapsed > time_budget_sec:
|
||||
print(f"\nTime budget {time_budget_sec}s exhausted at n = {n}.")
|
||||
return
|
||||
print(f"\n=== n = {n} === (elapsed {elapsed:.1f}s)")
|
||||
tcount = 0
|
||||
total_reductions = 0
|
||||
total_colorings = 0
|
||||
try:
|
||||
it = graphs.triangulations(n, minimum_degree=5)
|
||||
except Exception as ex:
|
||||
print(f" cannot enumerate triangulations: {ex}")
|
||||
n += 1
|
||||
continue
|
||||
for G in it:
|
||||
tcount += 1
|
||||
if not G.is_planar(set_embedding=True):
|
||||
continue
|
||||
D = dual_of(G)
|
||||
if not D.is_planar(set_embedding=True):
|
||||
continue
|
||||
faces_D = D.faces()
|
||||
pentagonal = [f for f in faces_D if len(f) == 5]
|
||||
for face in pentagonal:
|
||||
for i_red in range(5):
|
||||
H, named = apply_reduction(D, face, i_red)
|
||||
if H is None:
|
||||
continue
|
||||
total_reductions += 1
|
||||
edges, gen = proper_3_edge_colorings(H)
|
||||
# Find indices of named edges
|
||||
named_idx = {}
|
||||
for ii, e in enumerate(edges):
|
||||
es = frozenset((e[0], e[1]))
|
||||
for role, ns in named.items():
|
||||
if es == ns:
|
||||
named_idx[role] = ii
|
||||
if len(named_idx) != 4:
|
||||
continue
|
||||
for col in gen:
|
||||
total_colorings += 1
|
||||
if matches(edges, col, named_idx):
|
||||
report(edges, col, named_idx, named, {
|
||||
'n': n,
|
||||
'triangulation_index': tcount,
|
||||
'face_size': len(face),
|
||||
'i_red': i_red,
|
||||
'reduced_dual_V': H.order(),
|
||||
'reduced_dual_E': H.size(),
|
||||
})
|
||||
return
|
||||
sys.stdout.flush()
|
||||
print(f" n = {n}: {tcount} triangulation(s), "
|
||||
f"{total_reductions} reductions, "
|
||||
f"{total_colorings} colorings; no hit.")
|
||||
n += 1
|
||||
print(f"\nSearch exhausted up to n = {max_n}.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
search()
|
||||
Reference in New Issue
Block a user