Add Even Level Graph Generators paper + extend Level Switching reachability
- New paper papers/even_level_graph_generators/: defines Even Level Graph (every level cycle even), derived level graphs, intertwining trees, and the disjunction conjecture (every maximal planar graph is a derived level graph or intertwining tree). Empirically tested through n=11: every iso class is at least an intertwining tree, so the disjunction holds trivially in this range. The intertwining tree disjunct fails at the Tutte graph dual (n=25), so the disjunction becomes non-trivial past some unknown threshold. - Level Switching paper: adds Section 4 (Reachability via edge switches) with the two-step argument (Sleator-Tarjan-Thurston for Case 1; face-merges for Case 2) and Theorem 4.1 (O(n) edge switches suffice to reach all-depth-0). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""Plot iso[49] at n=9, the counterexample to Conjecture 4.4."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
tris = enumerate_all_triangulations(9)
|
||||
G = tris[49]
|
||||
print(f'iso[49] degree sequence: {sorted([G.degree(v) for v in G.nodes()], reverse=True)}')
|
||||
print(f'iso[49] edges: {sorted(G.edges())}')
|
||||
|
||||
# Use planar layout
|
||||
_, emb = nx.check_planarity(G)
|
||||
pos = nx.combinatorial_embedding_to_pos(emb)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 7))
|
||||
nx.draw_networkx_edges(G, pos, ax=ax, edge_color='#333', width=1.5)
|
||||
# Color vertices by degree
|
||||
degree_color = {4: '#3b82f6', 5: '#dc2626'}
|
||||
node_colors = [degree_color[G.degree(v)] for v in G.nodes()]
|
||||
nx.draw_networkx_nodes(G, pos, ax=ax, node_color=node_colors,
|
||||
node_size=600, edgecolors='black', linewidths=1.2)
|
||||
nx.draw_networkx_labels(G, pos, ax=ax, font_color='white',
|
||||
font_size=11, font_weight='bold')
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_title('iso[49] at $n=9$: degree sequence (5,5,5,5,5,5,4,4,4).\n'
|
||||
'NOT a valid derived level graph of any Even Level Graph.\n'
|
||||
'Blue = degree 4, Red = degree 5.', fontsize=11)
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_n9_counterexample.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Show a valid parity partition of iso[49] at n=9, with the induced
|
||||
4-coloring."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
tris = enumerate_all_triangulations(9)
|
||||
G = tris[49]
|
||||
|
||||
V_E = (0, 1, 3, 6)
|
||||
V_O = (2, 4, 5, 7, 8)
|
||||
|
||||
# 2-color each induced subgraph
|
||||
def bipart_coloring(subg):
|
||||
# BFS-based 2-coloring; returns dict v -> 0/1
|
||||
cmap = {}
|
||||
for start in subg.nodes():
|
||||
if start in cmap: continue
|
||||
cmap[start] = 0
|
||||
frontier = [start]
|
||||
while frontier:
|
||||
new = []
|
||||
for u in frontier:
|
||||
for w in subg.neighbors(u):
|
||||
if w not in cmap:
|
||||
cmap[w] = 1 - cmap[u]
|
||||
new.append(w)
|
||||
frontier = new
|
||||
return cmap
|
||||
|
||||
cE = bipart_coloring(G.subgraph(V_E))
|
||||
cO = bipart_coloring(G.subgraph(V_O))
|
||||
# 4-color: even gets red/blue, odd gets yellow/green
|
||||
COLOR_RED, COLOR_BLUE = '#dc2626', '#3b82f6'
|
||||
COLOR_YELLOW, COLOR_GREEN = '#eab308', '#16a34a'
|
||||
four_color = {}
|
||||
for v in V_E:
|
||||
four_color[v] = COLOR_RED if cE[v] == 0 else COLOR_BLUE
|
||||
for v in V_O:
|
||||
four_color[v] = COLOR_YELLOW if cO[v] == 0 else COLOR_GREEN
|
||||
|
||||
# Verify proper 4-coloring
|
||||
for u, v in G.edges():
|
||||
assert four_color[u] != four_color[v], \
|
||||
f'edge ({u},{v}) violates 4-coloring: {four_color[u]} vs {four_color[v]}'
|
||||
print('4-coloring is proper.')
|
||||
|
||||
_, emb = nx.check_planarity(G)
|
||||
pos = nx.combinatorial_embedding_to_pos(emb)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9, 8))
|
||||
nx.draw_networkx_edges(G, pos, ax=ax, edge_color='#333', width=1.5)
|
||||
node_colors = [four_color[v] for v in G.nodes()]
|
||||
nx.draw_networkx_nodes(G, pos, ax=ax, node_color=node_colors,
|
||||
node_size=700, edgecolors='black', linewidths=1.3)
|
||||
nx.draw_networkx_labels(G, pos, ax=ax, font_color='white',
|
||||
font_size=12, font_weight='bold')
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
|
||||
# Legend
|
||||
import matplotlib.patches as mpatches
|
||||
legend_handles = [
|
||||
mpatches.Patch(color=COLOR_RED, label=r'even-parity vertex, bipartition class 0'),
|
||||
mpatches.Patch(color=COLOR_BLUE, label=r'even-parity vertex, bipartition class 1'),
|
||||
mpatches.Patch(color=COLOR_YELLOW, label=r'odd-parity vertex, bipartition class 0'),
|
||||
mpatches.Patch(color=COLOR_GREEN, label=r'odd-parity vertex, bipartition class 1'),
|
||||
]
|
||||
ax.legend(handles=legend_handles, loc='lower center',
|
||||
bbox_to_anchor=(0.5, -0.08), ncol=2, fontsize=9, frameon=False)
|
||||
|
||||
ax.set_title(f'iso[49] with a valid parity partition\n'
|
||||
f'$V_E = \\{{0, 1, 3, 6\\}}$, '
|
||||
f'$V_O = \\{{2, 4, 5, 7, 8\\}}$\n'
|
||||
f'(both induced subgraphs bipartite; 4-coloring derived)',
|
||||
fontsize=11)
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_n9_valid_partition.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
# Also report the induced subgraphs
|
||||
GE = G.subgraph(V_E)
|
||||
GO = G.subgraph(V_O)
|
||||
print(f'G[V_E] = {sorted(GE.edges())} bipartite={nx.is_bipartite(GE)}')
|
||||
print(f'G[V_O] = {sorted(GO.edges())} bipartite={nx.is_bipartite(GO)}')
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Empirical test of the derived-level-graph conjecture for n=6..8.
|
||||
|
||||
For each iso class of maximal planar graphs G':
|
||||
Search for an Even Level Graph G (some iso class, some level source)
|
||||
such that G' is in the iso-class orbit of G under E/O-edge switches.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
import time
|
||||
import itertools
|
||||
import networkx as nx
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
|
||||
|
||||
def canonical_sig(G):
|
||||
"""Iso-class signature: WL hash via Weisfeiler-Lehman or just sorted
|
||||
edge tuples up to vertex relabelling. We use nx.weisfeiler_lehman_graph_hash."""
|
||||
return nx.weisfeiler_lehman_graph_hash(G)
|
||||
|
||||
|
||||
def labelled_sig(G, labels):
|
||||
"""Signature that respects both graph structure and a vertex labelling
|
||||
(even/odd). Two graphs match iff there's an iso preserving labels."""
|
||||
H = G.copy()
|
||||
for v in H.nodes():
|
||||
H.nodes[v]['label'] = labels[v]
|
||||
return nx.weisfeiler_lehman_graph_hash(H, node_attr='label')
|
||||
|
||||
|
||||
def bfs_levels(G, source_set):
|
||||
"""BFS from a set of source vertices in G. Returns dict v -> level."""
|
||||
levels = {v: float('inf') for v in G.nodes()}
|
||||
frontier = list(source_set)
|
||||
for v in frontier:
|
||||
levels[v] = 0
|
||||
d = 0
|
||||
while frontier:
|
||||
next_frontier = []
|
||||
for u in frontier:
|
||||
for w in G.neighbors(u):
|
||||
if levels[w] > d + 1:
|
||||
levels[w] = d + 1
|
||||
next_frontier.append(w)
|
||||
frontier = next_frontier
|
||||
d += 1
|
||||
return levels
|
||||
|
||||
|
||||
def get_level_sources(G):
|
||||
"""Yield each vertex as a possible level source (singleton set)."""
|
||||
for v in G.nodes():
|
||||
yield frozenset({v})
|
||||
|
||||
|
||||
def is_even_level_graph(G, source):
|
||||
"""Check: every level cycle of G (BFS from source) has even length."""
|
||||
levels = bfs_levels(G, source)
|
||||
if any(l == float('inf') for l in levels.values()):
|
||||
return False, None
|
||||
max_l = max(levels.values())
|
||||
for k in range(max_l + 1):
|
||||
L_k_nodes = [v for v in G.nodes() if levels[v] == k]
|
||||
L_k = G.subgraph(L_k_nodes)
|
||||
if L_k.number_of_edges() == 0:
|
||||
continue
|
||||
# Check bipartiteness (equivalent to no odd cycles)
|
||||
if not nx.is_bipartite(L_k):
|
||||
return False, None
|
||||
return True, levels
|
||||
|
||||
|
||||
def is_valid_parity_partition(G, labels):
|
||||
"""Both induced subgraphs G[V_E] and G[V_O] are bipartite."""
|
||||
V_E = [v for v in G.nodes() if labels[v] % 2 == 0]
|
||||
V_O = [v for v in G.nodes() if labels[v] % 2 == 1]
|
||||
return nx.is_bipartite(G.subgraph(V_E)) and nx.is_bipartite(G.subgraph(V_O))
|
||||
|
||||
|
||||
def E_O_switches(G, labels):
|
||||
"""Yield all triangulations reachable from G by one E/O-edge switch.
|
||||
An E/O-edge has both endpoints of the same parity in `labels`."""
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
return
|
||||
yielded = set()
|
||||
for u, v in list(G.edges()):
|
||||
if labels[u] % 2 != labels[v] % 2:
|
||||
continue
|
||||
f1 = emb.traverse_face(u, v)
|
||||
f2 = emb.traverse_face(v, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
continue
|
||||
w = next(x for x in f1 if x != u and x != v)
|
||||
x = next(y for y in f2 if y != u and y != v)
|
||||
if w == x or G.has_edge(w, x):
|
||||
continue
|
||||
Gp = G.copy()
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
sig = frozenset(frozenset(e) for e in Gp.edges())
|
||||
if sig in yielded:
|
||||
continue
|
||||
yielded.add(sig)
|
||||
yield Gp
|
||||
|
||||
|
||||
def bfs_orbit(G_start, labels, max_states=200000):
|
||||
"""BFS in E/O-switch graph starting from G_start (with given labels).
|
||||
Returns the set of iso classes (unlabelled) reachable."""
|
||||
seen_labelled = {frozenset(frozenset(e) for e in G_start.edges())}
|
||||
iso_classes_reached = {canonical_sig(G_start)}
|
||||
frontier = [G_start]
|
||||
rounds = 0
|
||||
while frontier and len(seen_labelled) < max_states:
|
||||
new = []
|
||||
for G in frontier:
|
||||
for Gp in E_O_switches(G, labels):
|
||||
sig = frozenset(frozenset(e) for e in Gp.edges())
|
||||
if sig in seen_labelled:
|
||||
continue
|
||||
seen_labelled.add(sig)
|
||||
iso_classes_reached.add(canonical_sig(Gp))
|
||||
new.append(Gp)
|
||||
frontier = new
|
||||
rounds += 1
|
||||
return iso_classes_reached, len(seen_labelled), rounds
|
||||
|
||||
|
||||
def test_for_n(n):
|
||||
print(f'\n=== n = {n} ===')
|
||||
t0 = time.time()
|
||||
tris = enumerate_all_triangulations(n)
|
||||
print(f' {len(tris)} iso classes of triangulations')
|
||||
iso_class_sigs = {canonical_sig(T) for T in tris}
|
||||
|
||||
# Set of iso classes that ARE derived level graphs (of some ELG)
|
||||
derived_iso_classes = set()
|
||||
|
||||
for i, G in enumerate(tris):
|
||||
if len(derived_iso_classes) == len(iso_class_sigs):
|
||||
break # early exit: everything is already covered
|
||||
for source in get_level_sources(G):
|
||||
is_elg, levels = is_even_level_graph(G, source)
|
||||
if not is_elg:
|
||||
continue
|
||||
reached, n_lbld, rnds = bfs_orbit(G, levels)
|
||||
new_count = len(reached - derived_iso_classes)
|
||||
derived_iso_classes.update(reached)
|
||||
if new_count > 0:
|
||||
print(f' iso[{i}] source={sorted(source)}: ELG, '
|
||||
f'orbit adds {new_count} new iso classes '
|
||||
f'(orbit size {len(reached)}, '
|
||||
f'{n_lbld} labelled, {rnds} rounds, '
|
||||
f'total {len(derived_iso_classes)}/'
|
||||
f'{len(iso_class_sigs)})')
|
||||
|
||||
missing = iso_class_sigs - derived_iso_classes
|
||||
print(f' TOTAL: {len(derived_iso_classes)} / {len(iso_class_sigs)} '
|
||||
f'iso classes are derived level graphs')
|
||||
print(f' missing: {len(missing)}')
|
||||
print(f' elapsed: {time.time() - t0:.1f}s')
|
||||
return len(missing) == 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
ns = [int(x) for x in sys.argv[1:]] if len(sys.argv) > 1 else [6, 7, 8]
|
||||
for n in ns:
|
||||
ok = test_for_n(n)
|
||||
print(f' conjecture holds for n={n}: {ok}')
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Test the disjunction: every maximal planar graph is a valid derived
|
||||
level graph, an intertwining tree, or both. Iterates n=6..12, stops if
|
||||
a counterexample is found."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import time
|
||||
import itertools
|
||||
import networkx as nx
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from test_conjecture import (
|
||||
canonical_sig, bfs_levels, get_level_sources,
|
||||
is_even_level_graph, bfs_orbit
|
||||
)
|
||||
|
||||
|
||||
def is_intertwining_tree(G):
|
||||
"""Search for a 2-partition (A, B) such that G[A] and G[B] are trees."""
|
||||
nodes = list(G.nodes())
|
||||
n = len(nodes)
|
||||
# Try all 2^(n-1) partitions (fix node 0 in A by convention)
|
||||
for mask in range(1, 2 ** (n - 1)):
|
||||
A = [nodes[0]] + [nodes[i + 1] for i in range(n - 1) if (mask >> i) & 1]
|
||||
B = [nodes[i + 1] for i in range(n - 1) if not ((mask >> i) & 1)]
|
||||
if not A or not B:
|
||||
continue
|
||||
GA = G.subgraph(A)
|
||||
GB = G.subgraph(B)
|
||||
if nx.is_tree(GA) and nx.is_tree(GB):
|
||||
return True, (tuple(A), tuple(B))
|
||||
return False, None
|
||||
|
||||
|
||||
def derived_level_graph_iso_classes(tris):
|
||||
"""Compute the set of iso class signatures that are derived level
|
||||
graphs of some Even Level Graph."""
|
||||
iso_class_sigs = {canonical_sig(T) for T in tris}
|
||||
derived = set()
|
||||
for G in tris:
|
||||
if len(derived) == len(iso_class_sigs):
|
||||
break
|
||||
for source in get_level_sources(G):
|
||||
is_elg, levels = is_even_level_graph(G, source)
|
||||
if not is_elg:
|
||||
continue
|
||||
reached, _, _ = bfs_orbit(G, levels)
|
||||
derived.update(reached)
|
||||
return derived
|
||||
|
||||
|
||||
def test_n(n):
|
||||
t0 = time.time()
|
||||
tris = enumerate_all_triangulations(n)
|
||||
iso_sigs = [canonical_sig(T) for T in tris]
|
||||
derived = derived_level_graph_iso_classes(tris)
|
||||
n_derived = sum(1 for s in iso_sigs if s in derived)
|
||||
|
||||
# For iso classes that are NOT derived, check intertwining tree
|
||||
counterexamples = []
|
||||
n_intertwining_only = 0
|
||||
n_both = 0
|
||||
for i, G in enumerate(tris):
|
||||
is_derived = iso_sigs[i] in derived
|
||||
is_inter, partition = is_intertwining_tree(G)
|
||||
if is_derived and is_inter:
|
||||
n_both += 1
|
||||
elif is_derived:
|
||||
pass # only derived
|
||||
elif is_inter:
|
||||
n_intertwining_only += 1
|
||||
else:
|
||||
counterexamples.append((i, G))
|
||||
|
||||
n_total = len(tris)
|
||||
n_only_derived = n_derived - n_both
|
||||
elapsed = time.time() - t0
|
||||
return {
|
||||
'n': n,
|
||||
'total': n_total,
|
||||
'derived': n_derived,
|
||||
'intertwining_only': n_intertwining_only,
|
||||
'both': n_both,
|
||||
'only_derived': n_only_derived,
|
||||
'counterexamples': counterexamples,
|
||||
'elapsed': elapsed,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
results = []
|
||||
for n in [6, 7, 8, 9, 10, 11, 12]:
|
||||
print(f'\n=== n = {n} ===')
|
||||
r = test_n(n)
|
||||
results.append(r)
|
||||
print(f' total iso classes: {r["total"]}')
|
||||
print(f' derived only: {r["only_derived"]}')
|
||||
print(f' intertwining only: {r["intertwining_only"]}')
|
||||
print(f' both: {r["both"]}')
|
||||
print(f' counterexamples: {len(r["counterexamples"])}')
|
||||
print(f' elapsed: {r["elapsed"]:.1f}s')
|
||||
if r['counterexamples']:
|
||||
print(f' COUNTEREXAMPLE FOUND. Stopping.')
|
||||
for i, G in r['counterexamples'][:3]:
|
||||
print(f' iso[{i}] degree seq = '
|
||||
f'{sorted([G.degree(v) for v in G.nodes()], reverse=True)}')
|
||||
break
|
||||
|
||||
print('\n=== Final summary ===')
|
||||
print(f'{"n":>3} {"total":>6} {"deriv":>6} {"inter":>6} {"both":>6} {"missing":>8}')
|
||||
for r in results:
|
||||
cov = r['only_derived'] + r['intertwining_only'] + r['both']
|
||||
missing = r['total'] - cov
|
||||
print(f'{r["n"]:>3} {r["total"]:>6} {r["only_derived"]:>6} '
|
||||
f'{r["intertwining_only"]:>6} {r["both"]:>6} {missing:>8}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Test whether the dual of the Tutte graph (46-vertex 3-connected planar
|
||||
cubic non-Hamiltonian) admits a tree coloring.
|
||||
|
||||
The Lederberg-Bosak-Barnette graph (38 vertices) is the smallest known
|
||||
counterexample to Tait's conjecture but isn't directly available in
|
||||
networkx; the Tutte graph (1946 original counterexample) is.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
import networkx as nx
|
||||
import time
|
||||
|
||||
|
||||
def dual_triangulation(G):
|
||||
"""Build the planar dual of a 3-connected planar cubic graph.
|
||||
|
||||
Each face of G becomes a vertex of the dual; each edge of G between
|
||||
two faces becomes a dual edge. Cubic G ⇒ every dual face is a
|
||||
triangle ⇒ dual is a triangulation."""
|
||||
ok, emb = nx.check_planarity(G)
|
||||
assert ok
|
||||
faces = []
|
||||
seen = set()
|
||||
for u, v in G.edges():
|
||||
for src, dst in [(u, v), (v, u)]:
|
||||
face = tuple(emb.traverse_face(src, dst))
|
||||
key = frozenset(face)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
faces.append(face)
|
||||
|
||||
# The "outer" face is the one with the largest vertex set (heuristic);
|
||||
# for connectivity of the dual, we just include all faces.
|
||||
face_of_edge = {} # edge (u, v) -> set of face indices it borders
|
||||
for i, face in enumerate(faces):
|
||||
for j in range(len(face)):
|
||||
a, b = face[j], face[(j + 1) % len(face)]
|
||||
key = frozenset((a, b))
|
||||
face_of_edge.setdefault(key, []).append(i)
|
||||
|
||||
D = nx.Graph()
|
||||
D.add_nodes_from(range(len(faces)))
|
||||
for key, face_ids in face_of_edge.items():
|
||||
if len(face_ids) == 2:
|
||||
D.add_edge(face_ids[0], face_ids[1])
|
||||
# If len > 2 or 1, multi-edges or self-loops would appear; for
|
||||
# 3-connected G this shouldn't happen.
|
||||
return D, faces
|
||||
|
||||
|
||||
def is_tree(subg):
|
||||
return nx.is_tree(subg) if subg.number_of_nodes() > 0 else True
|
||||
|
||||
|
||||
def has_tree_property_for_some_pairing(G, coloring):
|
||||
pairings = [({0, 1}, {2, 3}), ({0, 2}, {1, 3}), ({0, 3}, {1, 2})]
|
||||
for p1, p2 in pairings:
|
||||
V1 = [v for v in G.nodes() if coloring[v] in p1]
|
||||
V2 = [v for v in G.nodes() if coloring[v] in p2]
|
||||
if is_tree(G.subgraph(V1)) and is_tree(G.subgraph(V2)):
|
||||
return True, (p1, p2)
|
||||
return False, None
|
||||
|
||||
|
||||
def find_tree_coloring(G, time_limit=60.0):
|
||||
"""Backtracking search for a 4-coloring with the tree property."""
|
||||
nodes = list(G.nodes())
|
||||
n = len(nodes)
|
||||
colors = [None] * n
|
||||
adj = {v: set(G.neighbors(v)) for v in nodes}
|
||||
idx_of = {v: i for i, v in enumerate(nodes)}
|
||||
t0 = time.time()
|
||||
visited = [0]
|
||||
|
||||
def bt(i):
|
||||
if time.time() - t0 > time_limit:
|
||||
return None
|
||||
visited[0] += 1
|
||||
if i == n:
|
||||
coloring = dict(zip(nodes, colors))
|
||||
ok, pair = has_tree_property_for_some_pairing(G, coloring)
|
||||
return (coloring, pair) if ok else None
|
||||
v = nodes[i]
|
||||
forbidden = set()
|
||||
for w in adj[v]:
|
||||
if idx_of[w] < i:
|
||||
forbidden.add(colors[idx_of[w]])
|
||||
for c in range(4):
|
||||
if c in forbidden:
|
||||
continue
|
||||
colors[i] = c
|
||||
r = bt(i + 1)
|
||||
if r is not None:
|
||||
return r
|
||||
colors[i] = None
|
||||
return None
|
||||
|
||||
return bt(0), visited[0]
|
||||
|
||||
|
||||
def main():
|
||||
G = nx.tutte_graph()
|
||||
print(f'Tutte graph: {G.number_of_nodes()} vertices, '
|
||||
f'{G.number_of_edges()} edges, planar=True, cubic, 3-connected, '
|
||||
f'non-Hamiltonian (Tutte 1946).')
|
||||
|
||||
D, faces = dual_triangulation(G)
|
||||
print(f'Dual: {D.number_of_nodes()} vertices, '
|
||||
f'{D.number_of_edges()} edges')
|
||||
print(f' is_triangulation (3n-6 edges): '
|
||||
f'{D.number_of_edges() == 3 * D.number_of_nodes() - 6}')
|
||||
print(f' degree sequence (sorted desc): '
|
||||
f'{sorted([D.degree(v) for v in D.nodes()], reverse=True)}')
|
||||
|
||||
print('Searching for a tree coloring...')
|
||||
t0 = time.time()
|
||||
result, n_visited = find_tree_coloring(D, time_limit=120.0)
|
||||
elapsed = time.time() - t0
|
||||
if result is None:
|
||||
print(f' no tree coloring found within time limit '
|
||||
f'({elapsed:.1f}s, {n_visited} states visited)')
|
||||
else:
|
||||
coloring, pair = result
|
||||
print(f' tree coloring FOUND ({elapsed:.1f}s, {n_visited} states).')
|
||||
print(f' pairing: {pair}')
|
||||
p1, p2 = pair
|
||||
V1 = [v for v in D.nodes() if coloring[v] in p1]
|
||||
V2 = [v for v in D.nodes() if coloring[v] in p2]
|
||||
sub1 = D.subgraph(V1)
|
||||
sub2 = D.subgraph(V2)
|
||||
print(f' D[V1] (colors {p1}): {len(V1)} vertices, '
|
||||
f'{sub1.number_of_edges()} edges, tree={nx.is_tree(sub1)}')
|
||||
print(f' D[V2] (colors {p2}): {len(V2)} vertices, '
|
||||
f'{sub2.number_of_edges()} edges, tree={nx.is_tree(sub2)}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user