diff --git a/papers/coloring_nested_tire_graphs/experiments/draw_tire_tree_decomposition.py b/papers/coloring_nested_tire_graphs/experiments/draw_tire_tree_decomposition.py new file mode 100644 index 0000000..cd4740b --- /dev/null +++ b/papers/coloring_nested_tire_graphs/experiments/draw_tire_tree_decomposition.py @@ -0,0 +1,480 @@ +"""Draw the tire-tree decomposition for Theorem~\\ref{thm:tire-tree-decomposition}. + +Uses a Tutte embedding (barycentric solve with the outer face fixed on a +convex polygon) to give a planar straight-line layout. + +Panel (a): G with 13 vertices, 5 levels (v_0 at level 0; L_1, L_2, L_3, L_4 +on subsequent levels). Tutte outer face = h-g_1-g_2 (the deepest face from +v_0), so v_0 ends up roughly central and the outer triangle is "outermost" +in BFS-from-v_0 distance. Tree-of-treads has 5 nodes: + T_0 + / \\ + T_R T_L + | + T_LL + | + T_LLL + +Panel (b): G_{T_L} on 10 vertices, with Tutte outer face = C_{T_L} = {a,c,d}. +Levels in G_{T_L} satisfy ell_{G_{T_L}}(.) = ell_G(.) - 1 on V(G_{T_L}); +T(G_{T_L}, C_{T_L}) is the chain T_L -> T_LL -> T_LLL. +""" +import math +import os +import networkx as nx +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.lines import Line2D + +OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# --------------------------------------------------------------------------- +# Build G. +# --------------------------------------------------------------------------- +G = nx.Graph() +G.add_nodes_from(['v0', 'a', 'b', 'c', 'd', 'e', + 'f1', 'f2', 'f3', 'g1', 'g2', 'g3', 'h']) + +# v_0 fan +for v in ['a', 'b', 'c', 'd']: + G.add_edge('v0', v) +# L_1 4-cycle +G.add_edges_from([('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'a')]) +# Chord a-c (outside L_1 in the planar embedding) +G.add_edge('a', 'c') +# e-fan (Region R) +for v in ['a', 'b', 'c']: + G.add_edge('e', v) +# f-triangle (Region L) +G.add_edges_from([('f1', 'f2'), ('f2', 'f3'), ('f3', 'f1')]) +# Annular L_1 -> f-triangle: f_1~{a,d}, f_2~{d,c}, f_3~{a,c} +G.add_edges_from([ + ('f1', 'a'), ('f1', 'd'), + ('f2', 'd'), ('f2', 'c'), + ('f3', 'a'), ('f3', 'c'), +]) +# g-triangle (inside f-triangle) +G.add_edges_from([('g1', 'g2'), ('g2', 'g3'), ('g3', 'g1')]) +# Annular f-triangle -> g-triangle: f_1~{g_1, g_3}, f_2~{g_2, g_3}, f_3~{g_1, g_2} +G.add_edges_from([ + ('f1', 'g1'), ('f1', 'g3'), + ('f2', 'g2'), ('f2', 'g3'), + ('f3', 'g1'), ('f3', 'g2'), +]) +# h-fan (inside g-triangle) +for v in ['g1', 'g2', 'g3']: + G.add_edge('h', v) + +assert G.number_of_edges() == 3 * G.number_of_nodes() - 6, ( + f"G not a triangulation: |E|={G.number_of_edges()}, " + f"expected {3 * G.number_of_nodes() - 6}" +) + +level_G = nx.shortest_path_length(G, source='v0') + +# --------------------------------------------------------------------------- +# Tutte embedding. +# --------------------------------------------------------------------------- +def tutte_embedding(graph, outer_face_cyclic, outer_radius=3.0, + angle_offset_deg=90.0, outer_weight=1.0): + """Compute (weighted) Tutte embedding. + + outer_face_cyclic: list of vertices on the outer face, in cyclic order. + Outer vertices are fixed on a regular polygon of given radius; every + interior vertex is placed at a convex combination of its neighbours' + positions (solved as a linear system). Edges incident to an outer + vertex carry weight `outer_weight`; interior-interior edges carry + weight 1. All weights positive, so Tutte's theorem still gives a + planar straight-line embedding. Larger `outer_weight` pulls interior + vertices more strongly toward the outer triangle, spreading them out. + """ + n_outer = len(outer_face_cyclic) + outer_positions = {} + for i, v in enumerate(outer_face_cyclic): + angle = math.radians(angle_offset_deg + i * 360 / n_outer) + outer_positions[v] = (outer_radius * math.cos(angle), + outer_radius * math.sin(angle)) + + outer_set = set(outer_face_cyclic) + interior = [v for v in graph.nodes() if v not in outer_set] + interior_idx = {v: i for i, v in enumerate(interior)} + m = len(interior) + if m == 0: + return outer_positions + + A = np.zeros((m, m)) + bx = np.zeros(m) + by = np.zeros(m) + + for v in interior: + i = interior_idx[v] + for u in graph.neighbors(v): + w = outer_weight if u in outer_set else 1.0 + A[i, i] += w + if u in outer_set: + bx[i] += w * outer_positions[u][0] + by[i] += w * outer_positions[u][1] + else: + j = interior_idx[u] + A[i, j] -= w + + x = np.linalg.solve(A, bx) + y = np.linalg.solve(A, by) + + pos = dict(outer_positions) + for v in interior: + i = interior_idx[v] + pos[v] = (float(x[i]), float(y[i])) + return pos + + +def stretch_radially(pos, outer_face_cyclic, alpha=0.6): + """Stretch interior positions radially outward from the outer-face centroid. + + Outer vertices are unchanged; for each interior vertex at radius r from + the outer-face centroid, replace r by r' = R^(1-alpha) * r^alpha, where + R is the radius of the outer vertices. With alpha < 1 this is a concave + stretch that pushes small radii outward. Angles are preserved, so + planarity of the underlying Tutte embedding is preserved. + """ + outer_set = set(outer_face_cyclic) + outer_positions = [pos[v] for v in outer_face_cyclic] + Cx = sum(p[0] for p in outer_positions) / len(outer_positions) + Cy = sum(p[1] for p in outer_positions) / len(outer_positions) + R = max(math.sqrt((p[0] - Cx)**2 + (p[1] - Cy)**2) + for p in outer_positions) + + new_pos = {} + for v, p in pos.items(): + if v in outer_set: + new_pos[v] = p + continue + dx, dy = p[0] - Cx, p[1] - Cy + r = math.sqrt(dx * dx + dy * dy) + if r < 1e-10: + new_pos[v] = (Cx, Cy) + continue + r_new = (R ** (1 - alpha)) * (r ** alpha) + scale = r_new / r + new_pos[v] = (Cx + dx * scale, Cy + dy * scale) + return new_pos + + +# Panel (a) positions: hand-tuned in TikZiT and copied back here so the +# matplotlib panel matches the .tikz layout the user is editing on Desktop. +# The chord a-c is drawable as a straight line at these positions; the +# d-a edge passes through f_1, so it's drawn as a slight Bezier curve. +pos_G = { + 'v0': ( 0.00, 6.0), + 'a': (-5.50, -5.0), + 'b': (-2.75, 3.0), + 'c': ( 1.75, 3.0), + 'd': ( 8.00, -5.0), + 'e': (-1.50, 1.5), + 'f1': ( 1.50, -5.0), + 'f2': ( 4.00, -1.0), + 'f3': (-0.75, -1.0), + 'g1': ( 0.70, -3.0), + 'g2': ( 1.45, -1.5), + 'g3': ( 2.30, -3.0), + 'h': ( 1.55, -2.5), +} + +# --------------------------------------------------------------------------- +# G_{T_L}: subgraph inside C_{T_L} = {a, c, d}. +# --------------------------------------------------------------------------- +C_TL = {'a', 'c', 'd'} +V_GTL = C_TL | {'f1', 'f2', 'f3', 'g1', 'g2', 'g3', 'h'} +G_TL = G.subgraph(V_GTL).copy() +level_GTL = nx.multi_source_dijkstra_path_length(G_TL, C_TL) + +for v in V_GTL: + assert level_GTL[v] == level_G[v] - 1 + +# Tutte for G_{T_L}: outer face = a-d-c (= C_{T_L}, the seam itself). +# Cyclic order matches the planar boundary: a -> d -> c. +outer_face_GTL = ['a', 'd', 'c'] +pos_GTL = tutte_embedding(G_TL, outer_face_GTL, outer_radius=2.8, + angle_offset_deg=90, outer_weight=3.0) +pos_GTL = stretch_radially(pos_GTL, outer_face_GTL, alpha=0.6) + +# --------------------------------------------------------------------------- +# Edge sets. +# --------------------------------------------------------------------------- +C_TR_edges = [('a', 'b'), ('b', 'c'), ('a', 'c')] +C_TL_edges = [('a', 'd'), ('d', 'c'), ('a', 'c')] +C_TLL_edges = [('f1', 'f2'), ('f2', 'f3'), ('f3', 'f1')] +C_TLLL_edges = [('g1', 'g2'), ('g2', 'g3'), ('g3', 'g1')] +fan_edges = [('v0', v) for v in ['a', 'b', 'c', 'd']] + +# --------------------------------------------------------------------------- +# Tree inset helper. +# --------------------------------------------------------------------------- +def draw_tree_inset(parent_ax, position, tree_nodes, tree_edges, node_pos, + label_text, highlight=None, title=None, + xlim=(-1.4, 1.4), ylim=(-3.6, 0.6)): + ax = parent_ax.inset_axes(position) + for u, v in tree_edges: + x0, y0 = node_pos[u] + x1, y1 = node_pos[v] + ax.plot([x0, x1], [y0, y1], color='black', lw=1.4, zorder=1) + for n in tree_nodes: + x, y = node_pos[n] + is_hi = highlight and n in highlight + ax.scatter([x], [y], s=380, color='#fde68a' if is_hi else '#e5e7eb', + edgecolors='black', linewidths=1.8 if is_hi else 1.0, zorder=3) + ax.text(x, y, label_text[n], ha='center', va='center', + fontsize=7.5, fontweight='bold', zorder=4) + ax.set_xlim(*xlim); ax.set_ylim(*ylim) + ax.set_aspect('equal') + ax.set_xticks([]); ax.set_yticks([]) + if title: + ax.set_title(title, fontsize=8.5, pad=2) + for spine in ax.spines.values(): + spine.set_edgecolor('#9ca3af') + spine.set_linewidth(0.8) + ax.patch.set_facecolor('#f9fafb') + + +# --------------------------------------------------------------------------- +# Draw. +# --------------------------------------------------------------------------- +LEVEL_COLOR = { + 0: '#1e293b', + 1: '#475569', + 2: '#94a3b8', + 3: '#cbd5e1', + 4: '#f1f5f9', +} + +fig, axes = plt.subplots(1, 2, figsize=(16, 9)) + + +def _vertex_label(name): + """Render 'f1' as 'f_1' (subscript) and 'v0' as 'v_0' for display.""" + if len(name) >= 2 and name[0].isalpha() and name[1:].isdigit(): + return f'{name[0]}_{{{name[1:]}}}' + return name + + +def draw_panel(ax, graph, pos, levels, seams, fan, title, xlim, ylim, + text_color_threshold=4, label_map=None): + """seams: list of (edges, color, width) tuples; fan: edges list or None. + label_map: optional dict mapping vertex names to displayed names (so the + panel can show a relabelled version of the graph).""" + nx.draw_networkx_edges(graph, pos, ax=ax, edge_color='#d1d5db', width=1.1) + if fan is not None: + nx.draw_networkx_edges(graph, pos, edgelist=fan, ax=ax, + edge_color='#3b82f6', width=1.5, style='dashed') + for edges, color, width in seams: + nx.draw_networkx_edges(graph, pos, edgelist=edges, ax=ax, + edge_color=color, width=width) + for v, (x, y) in pos.items(): + lev = levels[v] + text_color = 'white' if lev < text_color_threshold else 'black' + display_name = (label_map or {}).get(v, v) + ax.scatter([x], [y], s=520, color=LEVEL_COLOR[lev], + edgecolors='black', linewidths=1.0, zorder=3) + ax.text(x, y, f'${_vertex_label(display_name)}$\n$\\ell{{=}}{lev}$', + ha='center', va='center', + color=text_color, fontsize=7, fontweight='bold', zorder=4) + ax.set_aspect('equal') + ax.axis('off') + ax.set_xlim(*xlim); ax.set_ylim(*ylim) + ax.set_title(title, fontsize=10.5, pad=5) + + +# Compute panel limits from the Tutte positions, with padding. +def compute_limits(pos, pad=0.5): + xs = [p[0] for p in pos.values()] + ys = [p[1] for p in pos.values()] + return (min(xs) - pad, max(xs) + pad), (min(ys) - pad, max(ys) + pad) + + +xlim_a, ylim_a = compute_limits(pos_G, pad=0.6) +xlim_b, ylim_b = compute_limits(pos_GTL, pad=0.6) + +# ============================================================================ +# Panel (a): G -- positions match Desktop/tire_tree_decomposition.tikz. +# Draw all non-seam edges as plain gray (no separate dashed fan style here, +# to match the TikZiT version). Edge a-d at y = -5 would pass through f_1 +# if drawn straight, so we render it (and seam C_{T_L}'s a-d component) as +# a slight Bezier curve via FancyArrowPatch. +# ============================================================================ +ax = axes[0] + +# Non-seam edges as straight gray lines; a-d and v_0-a are drawn curved +# below. +_curved_pairs = [{'a', 'd'}, {'v0', 'a'}] +non_seam_edges = [e for e in G.edges + if set(e) not in _curved_pairs + and set(e) not in (set(s) for s in C_TR_edges + C_TL_edges + + C_TLL_edges + C_TLLL_edges)] +nx.draw_networkx_edges(G, pos_G, edgelist=non_seam_edges, ax=ax, + edge_color='#d1d5db', width=1.1) + +# Curved edges (FancyArrowPatch). a-d dodges f_1; v_0-a is bent for +# visual balance (mirrors the TikZiT `bend right`). +from matplotlib.patches import FancyArrowPatch +def add_curved(ax, p_from, p_to, *, color, lw, rad, ls='-', zorder=2): + ax.add_patch(FancyArrowPatch( + posA=p_from, posB=p_to, + arrowstyle='-', connectionstyle=f'arc3,rad={rad}', + color=color, linewidth=lw, linestyle=ls, zorder=zorder, + )) +add_curved(ax, pos_G['a'], pos_G['d'], color='#d1d5db', lw=1.1, rad=0.15) +add_curved(ax, pos_G['v0'], pos_G['a'], color='#d1d5db', lw=1.1, rad=0.25) + +# Seams (straight, since the layout is now planar with straight chord). +# Order: bottom first (C_{T_LL}, C_{T_LLL}), then chord/L_1 seams on top. +nx.draw_networkx_edges(G, pos_G, + edgelist=[('f1', 'f2'), ('f2', 'f3'), ('f3', 'f1')], + ax=ax, edge_color='#9333ea', width=2.2) +nx.draw_networkx_edges(G, pos_G, + edgelist=[('g1', 'g2'), ('g2', 'g3'), ('g3', 'g1')], + ax=ax, edge_color='#0d9488', width=2.0) +nx.draw_networkx_edges(G, pos_G, + edgelist=[('a', 'b'), ('b', 'c')], + ax=ax, edge_color='#ea580c', width=2.4) +nx.draw_networkx_edges(G, pos_G, + edgelist=[('a', 'c'), ('d', 'c')], + ax=ax, edge_color='#dc2626', width=2.8) +add_curved(ax, pos_G['a'], pos_G['d'], color='#dc2626', lw=2.8, rad=0.15, zorder=4) + +# Vertices. +for v, (x, y) in pos_G.items(): + lev = level_G[v] + text_color = 'white' if lev < 4 else 'black' + ax.scatter([x], [y], s=520, color=LEVEL_COLOR[lev], + edgecolors='black', linewidths=1.0, zorder=5) + ax.text(x, y, f'${_vertex_label(v)}$\n$\\ell{{=}}{lev}$', + ha='center', va='center', + color=text_color, fontsize=7, fontweight='bold', zorder=6) + +ax.set_aspect('equal') +ax.axis('off') +# The a-d arc bulges below y=-5 by ~ rad * |a - d| / 2 ≈ 1.0 with rad=0.15; +# pad the y range so it isn't clipped. +xlim_a, ylim_a = compute_limits(pos_G, pad=0.8) +ylim_a = (ylim_a[0] - 1.2, ylim_a[1]) +ax.set_xlim(*xlim_a); ax.set_ylim(*ylim_a) +ax.set_title( + r"$(a)$ $G$: 13 vertices, BFS levels $\ell_G \in \{0,1,2,3,4\}$," + "\n" + r"four nested seams $C_{T_R}, C_{T_L}, C_{T_{LL}}, C_{T_{LLL}}$ " + r"(chord $a$-$c$ drawn straight in this layout).", + fontsize=10.5, pad=5, +) + +tree_pos_a = { + 'T_0': (0.0, 0.0), + 'T_R': (-0.7, -1.0), + 'T_L': (0.7, -1.0), + 'T_LL': (0.7, -2.0), + 'T_LLL': (0.7, -3.0), +} +tree_labels = { + 'T_0': r'$T_0$', 'T_R': r'$T_R$', 'T_L': r'$T_L$', + 'T_LL': r'$T_{LL}$', 'T_LLL': r'$T_{LLL}$', +} +draw_tree_inset( + axes[0], [0.70, 0.42, 0.28, 0.55], + tree_nodes=['T_0', 'T_R', 'T_L', 'T_LL', 'T_LLL'], + tree_edges=[('T_0', 'T_R'), ('T_0', 'T_L'), + ('T_L', 'T_LL'), ('T_LL', 'T_LLL')], + node_pos=tree_pos_a, label_text=tree_labels, + highlight={'T_L', 'T_LL', 'T_LLL'}, + title=r'$\mathcal{T}(G, \{v_0\})$', + xlim=(-1.4, 1.4), ylim=(-3.6, 0.6), +) + +# ============================================================================ +# Panel (b): G_{T_L}. Vertex names are rotated relative to the parent G +# (a->c, c->d, d->a; f_1->f_3, f_2->f_1, f_3->f_2; g_1->g_2, g_2->g_3, +# g_3->g_1; h stays h) -- the relabelling emphasises the new role of +# C_{T_L} as cycle source, with the boundary now naturally read as +# {a, c, d} in cyclic order from a position the user picked. +# ============================================================================ +GTL_LABEL_MAP = { + 'a': 'c', 'c': 'd', 'd': 'a', + 'f1': 'f3', 'f2': 'f1', 'f3': 'f2', + 'g1': 'g2', 'g2': 'g3', 'g3': 'g1', + 'h': 'h', +} + +draw_panel( + axes[1], G_TL, pos_GTL, level_GTL, + seams=[ + (C_TL_edges, '#dc2626', 2.8), + (C_TLL_edges, '#9333ea', 2.2), + (C_TLLL_edges, '#0d9488', 2.0), + ], + fan=None, + title=( + r"$(b)$ $G_{T_L}$ (Tutte embedding, outer face $C_{T_L} = \{a,c,d\}$):" + " 10 vertices, cycle source $C_{T_L}$,\n" + r"$\ell_{G_{T_L}}(\cdot) = \ell_G(\cdot) - 1$; " + r"$\mathcal{T}(G_{T_L}, C_{T_L})$ is the chain $T_L \to T_{LL} \to T_{LLL}$." + ), + xlim=xlim_b, ylim=ylim_b, + text_color_threshold=3, + label_map=GTL_LABEL_MAP, +) + +sub_tree_pos = { + 'T_L': (0.0, 0.0), + 'T_LL': (0.0, -1.0), + 'T_LLL': (0.0, -2.0), +} +draw_tree_inset( + axes[1], [0.70, 0.42, 0.28, 0.55], + tree_nodes=['T_L', 'T_LL', 'T_LLL'], + tree_edges=[('T_L', 'T_LL'), ('T_LL', 'T_LLL')], + node_pos=sub_tree_pos, label_text=tree_labels, + highlight={'T_L', 'T_LL', 'T_LLL'}, + title=r'$\mathcal{T}(G_{T_L}, C_{T_L})$', + xlim=(-1.0, 1.0), ylim=(-2.6, 0.4), +) + +# ============================================================================ +# Legend + suptitle +# ============================================================================ +legend = [ + Line2D([0], [0], marker='o', color='w', label=r'$\ell = 0$', + markerfacecolor=LEVEL_COLOR[0], markeredgecolor='black', markersize=10), + Line2D([0], [0], marker='o', color='w', label=r'$\ell = 1$', + markerfacecolor=LEVEL_COLOR[1], markeredgecolor='black', markersize=10), + Line2D([0], [0], marker='o', color='w', label=r'$\ell = 2$', + markerfacecolor=LEVEL_COLOR[2], markeredgecolor='black', markersize=10), + Line2D([0], [0], marker='o', color='w', label=r'$\ell = 3$', + markerfacecolor=LEVEL_COLOR[3], markeredgecolor='black', markersize=10), + Line2D([0], [0], marker='o', color='w', label=r'$\ell = 4$', + markerfacecolor=LEVEL_COLOR[4], markeredgecolor='black', markersize=10), + Line2D([0], [0], color='#ea580c', lw=2.4, label=r'seam $C_{T_R}$'), + Line2D([0], [0], color='#dc2626', lw=2.8, label=r'seam $C_{T_L}$'), + Line2D([0], [0], color='#9333ea', lw=2.2, label=r'seam $C_{T_{LL}}$'), + Line2D([0], [0], color='#0d9488', lw=2.0, label=r'seam $C_{T_{LLL}}$'), +] +fig.legend(handles=legend, loc='lower center', ncol=5, fontsize=9.5, + framealpha=0.95, columnspacing=1.8) + +fig.suptitle( + r"Tire-tree decomposition (Theorem 1.19): every tread $T$ is the root " + r"of its own tree of tire treads $\mathcal{T}(G_T, C_T)$.", + fontsize=12, y=0.96, +) +fig.subplots_adjust(top=0.88, bottom=0.16, left=0.02, right=0.98, wspace=0.05) + +out = os.path.join(OUT_DIR, 'fig_tire_tree_decomposition.png') +fig.savefig(out, dpi=180, bbox_inches='tight') +plt.close(fig) + +print(f'G: |V|={G.number_of_nodes()}, |E|={G.number_of_edges()}, ' + f'levels {sorted(set(level_G.values()))}') +print(f'G_TL: |V|={G_TL.number_of_nodes()}, |E|={G_TL.number_of_edges()}, ' + f'levels {sorted(set(level_GTL.values()))}') +print(f'verified level shift (D2): ell_GTL(v) = ell_G(v) - 1 for all ' + f'{len(V_GTL)} v in V(G_TL)') +print(f'panel (a): hand-tuned positions (see Desktop/tire_tree_decomposition.tikz)') +print(f'Tutte (G_TL): outer face = {outer_face_GTL}') +print(f'wrote {out}') diff --git a/papers/coloring_nested_tire_graphs/fig_tire_tree_decomposition.png b/papers/coloring_nested_tire_graphs/fig_tire_tree_decomposition.png new file mode 100644 index 0000000..09152d0 Binary files /dev/null and b/papers/coloring_nested_tire_graphs/fig_tire_tree_decomposition.png differ diff --git a/papers/coloring_nested_tire_graphs/paper.aux b/papers/coloring_nested_tire_graphs/paper.aux index 48210bc..aaa0208 100644 --- a/papers/coloring_nested_tire_graphs/paper.aux +++ b/papers/coloring_nested_tire_graphs/paper.aux @@ -30,6 +30,14 @@ \newlabel{rem:count-general-outerplanar}{{1.16}{10}} \newlabel{thm:tread-tree}{{1.17}{10}} \newlabel{rem:tree-multiple-children}{{1.18}{11}} +\newlabel{thm:tire-tree-decomposition}{{1.19}{12}} +\newlabel{rem:tree-coloring-factorisation}{{1.20}{13}} +\@writefile{lof}{\contentsline {figure}{\numberline {5}{\ignorespaces Tire-tree decomposition (Theorem\nonbreakingspace 1.19\hbox {}) on a $13$-vertex maximal planar example $G$ with five BFS levels. $(a)$ $G$ with vertex source $v_0$ and $\ell _G \in \{0,1,2,3,4\}$; four nested seams are highlighted, $C_{T_R} = \{a,b,c\}$ (orange), $C_{T_L} = \{a,c,d\}$ (red, including the chord $a$-$c$ shared with $C_{T_R}$), $C_{T_{LL}} = \{f_1, f_2, f_3\}$ (purple), $C_{T_{LLL}} = \{g_1, g_2, g_3\}$ (teal). Inset: the rooted tree of tire treads $\mathcal {T}(G, \{v_0\})$ branches at $T_0$ into the leaf $T_R$ (containing $e$) and a chain $T_L \to T_{LL} \to T_{LLL}$ (the highlighted sub-tree). $(b)$ The disk $G_{T_L}$ inside the seam $C_{T_L}$, drawn standalone with $C_{T_L}$ as cycle source and vertex labels rotated to match the new (cycle-source) role of the boundary triangle. $\ell _{G_{T_L}}(\cdot ) = \ell _G(\cdot ) - 1$ on $V(G_{T_L})$ (verified by the generator script), and $\mathcal {T}(G_{T_L}, C_{T_L})$ is the chain $T_L \to T_{LL} \to T_{LLL}$, iso to the highlighted sub-tree of $(a)$.}}{14}{}\protected@file@percent } +\newlabel{fig:tire-tree-decomposition}{{5}{14}} +\newlabel{def:seam}{{1.21}{14}} +\newlabel{def:partial-tire-tree}{{1.22}{15}} +\newlabel{lem:seam-edge-shared}{{1.23}{15}} +\newlabel{conj:seam-counterexample}{{1.24}{15}} \bibcite{tait-original}{1} \bibcite{bauerfeld-depth}{2} \bibcite{bauerfeld-nested-tire-duals}{3} @@ -38,6 +46,5 @@ \newlabel{tocindent1}{17.77782pt} \newlabel{tocindent2}{0pt} \newlabel{tocindent3}{0pt} -\newlabel{rem:tree-coloring-factorisation}{{1.19}{12}} -\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{12}{}\protected@file@percent } -\gdef \@abspage@last{12} +\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{16}{}\protected@file@percent } +\gdef \@abspage@last{16} diff --git a/papers/coloring_nested_tire_graphs/paper.log b/papers/coloring_nested_tire_graphs/paper.log index 667ad80..c12cc92 100644 --- a/papers/coloring_nested_tire_graphs/paper.log +++ b/papers/coloring_nested_tire_graphs/paper.log @@ -1,4 +1,4 @@ -This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 27 MAY 2026 04:39 +This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 27 MAY 2026 22:18 entering extended mode restricted \write18 enabled. %&-line parsing enabled. @@ -498,58 +498,76 @@ e File: fig_dual_depth.png Graphic file (type png) -Package pdftex.def Info: fig_dual_depth.png used on input line 124. +Package pdftex.def Info: fig_dual_depth.png used on input line 128. (pdftex.def) Requested size: 251.9989pt x 237.67276pt. [2 <./fig_dual_depth.png>] File: fig_tire_example.png Graphic file (type png) -Package pdftex.def Info: fig_tire_example.png used on input line 179. +Package pdftex.def Info: fig_tire_example.png used on input line 183. (pdftex.def) Requested size: 280.79956pt x 188.56097pt. [3 <./fig_tire_example.png>] [4] [5] [6] LaTeX Warning: `h' float specifier changed to `ht'. -[7] [8] [9] [10] [11] [12] (./paper.aux) ) +[7] [8] [9] [10] [11] [12] + +File: fig_tire_tree_decomposition.png Graphic file (type png) + +Package pdftex.def Info: fig_tire_tree_decomposition.png used on input line 10 +98. +(pdftex.def) Requested size: 341.9989pt x 196.86678pt. + + +LaTeX Warning: `h' float specifier changed to `ht'. + +[13] [14 <./fig_tire_tree_decomposition.png>] +Overfull \hbox (1.78508pt too wide) in paragraph at lines 1228--1230 +[]\OT1/cmr/m/n/10 Length lower bound (Birkhoff). \OT1/cmr/m/it/10 Ev-ery non-tr +ivial seam $\OML/cmm/m/it/10 C$ \OT1/cmr/m/it/10 of $\OML/cmm/m/it/10 G$ \OT1/c +mr/m/it/10 has $\OMS/cmsy/m/n/10 j\OML/cmm/m/it/10 V\OT1/cmr/m/n/10 (\OML/cmm/m +/it/10 C\OT1/cmr/m/n/10 )\OMS/cmsy/m/n/10 j ^^U + [] + +[15] [16] (./paper.aux) ) Here is how much of TeX's memory you used: - 14048 strings out of 478268 - 279229 string characters out of 5846347 - 563840 words of memory out of 5000000 - 31872 multiletter control sequences out of 15000+600000 + 14061 strings out of 478268 + 279616 string characters out of 5846347 + 563910 words of memory out of 5000000 + 31884 multiletter control sequences out of 15000+600000 478218 words of font info for 62 fonts, out of 8000000 for 9000 1302 hyphenation exceptions out of 8191 - 84i,12n,89p,1156b,803s stack positions out of 10000i,1000n,20000p,200000b,200000s - - -Output written on paper.pdf (12 pages, 618557 bytes). + 84i,12n,89p,1168b,803s stack positions out of 10000i,1000n,20000p,200000b,200000s +< +/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb> +Output written on paper.pdf (16 pages, 927226 bytes). PDF statistics: - 177 PDF objects out of 1000 (max. 8388607) - 107 compressed objects within 2 object streams + 192 PDF objects out of 1000 (max. 8388607) + 116 compressed objects within 2 object streams 0 named destinations out of 1000 (max. 500000) - 23 words of extra memory for PDF output out of 10000 (max. 10000000) + 28 words of extra memory for PDF output out of 10000 (max. 10000000) diff --git a/papers/coloring_nested_tire_graphs/paper.pdf b/papers/coloring_nested_tire_graphs/paper.pdf index 4732408..e57a485 100644 Binary files a/papers/coloring_nested_tire_graphs/paper.pdf and b/papers/coloring_nested_tire_graphs/paper.pdf differ diff --git a/papers/coloring_nested_tire_graphs/paper.tex b/papers/coloring_nested_tire_graphs/paper.tex index db444fb..2f64217 100644 --- a/papers/coloring_nested_tire_graphs/paper.tex +++ b/papers/coloring_nested_tire_graphs/paper.tex @@ -87,8 +87,12 @@ with a fixed planar embedding $\Pi_G$. We write $|V| = n$, so $|E| = 3n - 6$ and $G$ has $2n - 4$ triangular faces. \begin{definition}[Level source] -A \emph{level source} of $G$ is any vertex $v \in V$; we write -$S = \{v\}$ for the level-0 source. +A \emph{level source} of $G$ is a set $S \subseteq V$ that is either +\begin{itemize} +\item a single vertex $\{v\}$ (a \emph{vertex source}), or +\item a set inducing a simple cycle in $G$ --- i.e.\ $G[S]$ is a + simple cycle of length $\geq 3$ (a \emph{cycle source}). +\end{itemize} \end{definition} \begin{definition}[Levels] @@ -932,6 +936,186 @@ of Remark~\ref{rem:hamilton-cycle-spoke-only}) with a non-empty interior, $T_p$ has exactly one child. \end{remark} +\begin{theorem}[Tire-tree decomposition] +\label{thm:tire-tree-decomposition} +Let $G$ be a maximal planar graph with planar embedding $\Pi_G$ and let +$v_0 \in V(G)$. The tree of tire treads $\mathcal{T}(G, \{v_0\})$ of +Theorem~\ref{thm:tread-tree} \emph{decomposes $G$ into nested tires}: +it is a finite rooted tree, rooted at the depth-$0$ tread containing +$v_0$, whose nodes (tire treads) partition the bounded faces of $G$ +(Theorem~\ref{thm:tread-partition}). + +This decomposition is moreover \emph{self-similar}. For any tread $T$ +in $\mathcal{T}(G, \{v_0\})$ at depth $d \ge 1$, with outer-boundary +cycle $C_T := B_{\mathrm{out}}^{(T)}$, let $G_T$ be the sub-graph of $G$ +induced by $C_T$ together with all vertices of $G$ lying in the closed +planar region $R_T \subset |\Pi_G|$ bounded by $C_T$ on the side of +$C_T$ away from $v_0$. Then: +\begin{enumerate} +\item[(D1)] $G_T$, with the embedding inherited from $\Pi_G$, is a + \emph{triangulated disk}: every bounded face is a triangle, + and the outer face is bounded by $C_T$. +\item[(D2)] Taking $C_T$ as a cycle source of $G_T$ (so $C_T$ has + level $0$ in $G_T$ and the BFS-from-$C_T$ levels in $G_T$ + equal $\ell_G(\cdot) - d$ on $V(G_T)$), the construction of + Theorem~\ref{thm:tread-tree} extends to give a rooted tree + of tire treads $\mathcal{T}(G_T, C_T)$ whose depth-$0$ root + tread has $B_{\mathrm{out}} = C_T$ and inner outerplanar + graph $O = O^{(T)}$. +\item[(D3)] $\mathcal{T}(G_T, C_T)$ is canonically iso to the sub-tree + of $\mathcal{T}(G, \{v_0\})$ rooted at $T$, preserving + outer-boundary cycles, inner outerplanar graphs, and the + parent--child face correspondence. +\end{enumerate} + +In short: pick any vertex $v_0 \in V(G)$ to root the global tree +$\mathcal{T}(G, \{v_0\})$ describing the whole graph; pick any tread $T$ +in this tree; then $T$ is itself the root of a local tree +$\mathcal{T}(G_T, C_T)$ describing the triangulated disk of $G$ inside +$C_T$, with $C_T$ as cycle source. Maximal planar graphs decompose +into nested trees of tire treads. +\end{theorem} + +\begin{proof} +\emph{Decomposition.} Theorem~\ref{thm:tread-tree} gives the rooted +tree structure of $\mathcal{T}(G, \{v_0\})$, with root the depth-$0$ +tread containing $v_0$; Theorem~\ref{thm:tread-partition} gives that +its tire treads partition the bounded faces of $G$. Finiteness of +the tree is immediate from finiteness of $G$. + +\emph{(D1) $G_T$ is a triangulated disk.} By +Lemma~\ref{lem:tire-component} applied to the component of $G'_d$ that +gives rise to $T$, the outer boundary $C_T = B_{\mathrm{out}}^{(T)}$ is +a simple cycle in $L_d^G$. By the Jordan curve theorem, $C_T$ +separates $|\Pi_G| \setminus C_T$ into two open regions; $R_T$ is the +closure of the one not containing $v_0$. The bounded faces of $G_T$ in +its inherited embedding are exactly the bounded faces of $G$ contained +in $R_T$, each of which is a triangle since $G$ is a triangulation. +The unbounded face of $G_T$'s embedding is the complement of $R_T$, +whose boundary is $C_T$. + +\emph{(D2) Level shift.} We show $\mathrm{dist}_{G_T}(v, C_T) = +\ell_G(v) - d$ for every $v \in V(G_T)$. When $v \in C_T$ both sides +equal $0$, so fix $v \in V(G_T) \setminus C_T$. + +\smallskip + +\emph{Step 1: $\mathrm{dist}_G(v, C_T) = \ell_G(v) - d$.} A shortest +$G$-path from $v$ to $v_0$ must visit $C_T$, since $v$ and $v_0$ lie in +different open regions of $|\Pi_G| \setminus C_T$; let $w$ be its first +$C_T$-vertex. The $v$-to-$w$ sub-path has length $\ge \mathrm{dist}_G(v, +C_T)$ and the $w$-to-$v_0$ sub-path has length $\ell_G(w) = d$, so +$\ell_G(v) \ge \mathrm{dist}_G(v, C_T) + d$. Conversely, concatenating +a shortest $G$-path from $v$ to a nearest $C_T$-vertex $w'$ with a +shortest $G$-path from $w'$ to $v_0$ gives a $v$-to-$v_0$ path of +length $\mathrm{dist}_G(v, C_T) + d$, so $\ell_G(v) \le +\mathrm{dist}_G(v, C_T) + d$. + +\smallskip + +\emph{Step 2: $\mathrm{dist}_{G_T}(v, C_T) = \mathrm{dist}_G(v, C_T)$.} +The inequality $\ge$ is automatic since $G_T \subseteq G$. For $\le$, +pick a shortest $G$-path $\pi$ from $v$ to $C_T$; we may assume $\pi$ +has no internal vertex in $C_T$ (truncate otherwise). Any internal +vertex of $\pi$ then lies in the same open region of $|\Pi_G| \setminus +C_T$ as $v$, i.e.\ in $R_T \setminus C_T \subseteq V(G_T)$; every edge +of $\pi$ has both endpoints in $V(G_T)$ and so lies in $E(G_T)$. Hence +$\pi$ is a path in $G_T$ realising $\mathrm{dist}_G(v, C_T)$. + +\smallskip + +Combining the two steps yields $\mathrm{dist}_{G_T}(v, C_T) = \ell_G(v) +- d$, as claimed. + +\emph{(D3) Tree iso.} By (D2), $L_k^{G_T} = L_{d+k}^G \cap V(G_T)$ for +every $k \ge 0$. For a bounded face $f$ of $G_T$, dual depth in $G_T$ +equals $\min_{u \in V(f)} \ell_{G_T}(u) = \min_{u \in V(f)} \ell_G(u) - +d = \delta_G(d_f) - d$. Hence the inner-dual subgraph $(G_T)'_{k}$ at +depth $k$ in $G_T$ is the induced subgraph of $G'_{d+k}$ on the faces +of $G$ lying in $R_T$, and two such faces are dual-adjacent in $G_T'$ +iff they are dual-adjacent in $G'$ (the shared edge is in $E(G_T)$). + +\smallskip + +\emph{Step 3: components of $(G_T)'_{k}$ are precisely the depth-$(d+k)$ +descendants of $T$ in $\mathcal{T}(G, \{v_0\})$.} We show by induction +on $k$ that a component $C'$ of $G'_{d+k}$ has $F_{C'} \subseteq R_T$ +iff $C'$ is a depth-$(d+k)$ descendant of $T$. + +For $k = 0$: the components of $G'_d$ are the depth-$d$ treads; the +component giving rise to $T$ has its faces in $T$'s tread region +$R \subseteq R_T$, while any other depth-$d$ tread $T''$ has +$C_{T''}$ disjoint from $C_T$ and lying in a different bounded face of +$O^{(T_p'')}$ at depth $d - 1$, hence $R_{T''} \cap R_T = \emptyset$. + +For $k \ge 1$: by Theorem~\ref{thm:tread-tree}, each component $C'$ of +$G'_{d+k}$ has a unique parent $C'_p$ at depth $d+k-1$, with +$B_{\mathrm{out}}^{(C')}$ bounding a face of $O^{(C'_p)}$; equivalently +$R_{C'}$ lies inside that bounded face, hence inside $R_{C'_p}$. By +the induction hypothesis $R_{C'_p} \subseteq R_T$ iff $C'_p$ is a +descendant of $T$ at depth $d+k-1$, and $R_{C'} \subseteq R_{C'_p}$, so +$R_{C'} \subseteq R_T$ iff $C'$ is a descendant of $T$ at depth $d+k$. + +\smallskip + +\emph{Step 4: tread data and child--face correspondence.} The +Tire-component lemma (Lemma~\ref{lem:tire-component}) and the +source-side simple-cycle property +(Proposition~\ref{prop:no-level-d-pinch}) extend verbatim to the +cycle-sourced triangulated disk $(G_T, C_T)$: the proofs use only +the triangular structure of bounded faces, the local arrangement of +faces around each vertex's rotation, and the connectivity of the BFS +ball $G_T[L_{