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:
+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