"""Stress-test the preprocessing algorithm on random maximal-outerplanar graphs of varying size. Track whether the algorithm always reaches all-depth-0 and how many steps it takes.""" import random import sys import networkx as nx from collections import Counter def face_edges(f): return {frozenset((f[0], f[1])), frozenset((f[1], f[2])), frozenset((f[0], f[2]))} def random_triangulation(num_vertices, seed=None): """Random triangulation of a convex polygon on `num_vertices` vertices. Returns (outer_edges, chords, faces). Uses a recursive splitting algorithm. The polygon vertices are labelled 0..n-1 around the outer cycle.""" rng = random.Random(seed) n = num_vertices outer = [(i, (i + 1) % n) for i in range(n)] chords = [] faces = [] def split(start, end): """Triangulate the sub-polygon from `start` to `end` (inclusive), going through vertices start, start+1, ..., end on the outer cycle. The "chord" closing this sub-polygon is (start, end).""" if (end - start) % n <= 1: return if (end - start) % n == 2: mid = (start + 1) % n faces.append(tuple(sorted((start, mid, end)))) return # Pick a random vertex strictly between start and end as the apex length = (end - start) % n offset = rng.randint(1, length - 1) mid = (start + offset) % n faces.append(tuple(sorted((start, mid, end)))) if mid != (start + 1) % n: chords.append(tuple(sorted((start, mid)))) split(start, mid) if end != (mid + 1) % n: chords.append(tuple(sorted((mid, end)))) split(mid, end) # Start by picking an outer-cycle vertex as the "root" for splitting; # we triangulate the polygon as if the chord 0--(n-1) were closing # the sub-polygon on the other side. To get a proper triangulation # of the polygon, we use vertex 0 as the splitting root. split(0, n - 1) # Note: this doesn't add a chord from 0 to n-1 since that's an # outer edge. return outer, chords, faces def compute_depths(faces, outer_set): D = nx.Graph() D.add_nodes_from(range(len(faces))) for i, fi in enumerate(faces): for j, fj in enumerate(faces): if i < j and face_edges(fi) & face_edges(fj): D.add_edge(i, j) B = [i for i, f in enumerate(faces) if len(face_edges(f) & outer_set) >= 1] if not B: return {i: float('inf') for i in range(len(faces))} return {i: min(nx.shortest_path_length(D, i, b) for b in B) for i in range(len(faces))} def check_balanced(F_idx, faces, depth_, outer_set): F = faces[F_idx] fe = face_edges(F) for e in fe: if e in outer_set: continue cands = [j for j in range(len(faces)) if j != F_idx and e in face_edges(faces[j])] if not cands: continue Fp_idx = cands[0] if depth_[Fp_idx] != depth_[F_idx] - 1: continue Fp = faces[Fp_idx] d = depth_[F_idx] ok = True for e2 in face_edges(Fp): if e2 == e: continue if e2 in outer_set: continue others = [j for j in range(len(faces)) if j != Fp_idx and e2 in face_edges(faces[j])] if not others or depth_[others[0]] != d - 2: ok = False break if ok: return True, F_idx, Fp_idx, e return False, None, None, None def apply_switch(faces, uv, wx): u, v = uv w, x = wx new_faces = [f for f in faces if set(f) != {u, v, w} and set(f) != {u, v, x}] new_faces.append(tuple(sorted((u, w, x)))) new_faces.append(tuple(sorted((v, w, x)))) return new_faces def run_algorithm(faces, outer_set, max_steps=2000, seed=None): rng = random.Random(seed) balanced_steps = 0 preprocess_steps = 0 depth_history = [] for step in range(max_steps): depth = compute_depths(faces, outer_set) d_max = max(depth.values()) depth_history.append(d_max) if d_max == 0: return {'terminated': True, 'steps': step, 'balanced': balanced_steps, 'preprocess': preprocess_steps, 'depth_history': depth_history} # Pick any maximum-depth face max_d_faces = [i for i, d in depth.items() if d == d_max] F_idx = rng.choice(max_d_faces) F = faces[F_idx] ok, _, fp_idx, e = check_balanced(F_idx, faces, depth, outer_set) if ok: Fp = faces[fp_idx] u, v = tuple(e) w = [vert for vert in F if vert not in (u, v)][0] x = [vert for vert in Fp if vert not in (u, v)][0] faces = apply_switch(faces, (u, v), (w, x)) balanced_steps += 1 continue # Preprocess: pick a random depth-(d-1) neighbour choices = [] for e_test in [frozenset((F[0], F[1])), frozenset((F[1], F[2])), frozenset((F[0], F[2]))]: if e_test in outer_set: continue cands = [j for j in range(len(faces)) if j != F_idx and e_test in face_edges(faces[j])] if cands and depth[cands[0]] == d_max - 1: choices.append((e_test, cands[0])) if not choices: return {'terminated': False, 'reason': 'no depth-(d-1) neighbour', 'depth_history': depth_history, 'final_depth': d_max} e, fp_idx = rng.choice(choices) Fp = faces[fp_idx] u, v = tuple(e) w = [vert for vert in F if vert not in (u, v)][0] x = [vert for vert in Fp if vert not in (u, v)][0] faces = apply_switch(faces, (u, v), (w, x)) preprocess_steps += 1 return {'terminated': False, 'reason': 'max_steps reached', 'depth_history': depth_history, 'final_depth': max(compute_depths(faces, outer_set).values())} if __name__ == '__main__': results = [] sizes = [10, 14, 18, 24, 30, 40, 60, 80] trials_per_size = 20 for n in sizes: n_terminated = 0 max_steps = 0 max_init_depth = 0 failed = [] for trial in range(trials_per_size): seed = hash((n, trial)) & 0xffffffff outer, chords, faces = random_triangulation(n, seed=seed) outer_set = {frozenset(e) for e in outer} init_depth = max(compute_depths(faces, outer_set).values()) max_init_depth = max(max_init_depth, init_depth) result = run_algorithm(faces, outer_set, max_steps=10 * len(faces), seed=seed + 1) if result['terminated']: n_terminated += 1 max_steps = max(max_steps, result['steps']) else: failed.append((seed, result)) print(f'n={n}: {n_terminated}/{trials_per_size} terminated; ' f'max init depth {max_init_depth}, max steps {max_steps}') if failed: for seed, res in failed[:3]: print(f' FAIL seed={seed}: {res}') results.append((n, failed)) if not results: print('\nAll trials terminated successfully.') else: print(f'\n{sum(len(f) for _, f in results)} failures across {len(results)} sizes')