mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
This squashes all local history (768 commits) onto the scrubbed PR #90 baseline. Individual commits were lost during filter-repo corruption; the working tree content is preserved intact. Build: N/A (working tree state only)
126 lines
4.8 KiB
Python
126 lines
4.8 KiB
Python
"""Chinese Postman Problem / Route Inspection / "edge-TSP" — Hierholzer +
|
||
minimum-weight perfect matching on odd-degree vertices.
|
||
|
||
Reference: mei-Ko Kwan (1962). For a connected undirected graph with
|
||
non-negative edge weights, the minimum closed walk covering every edge
|
||
has total weight:
|
||
|
||
sum_e w(e) + (minimum weight perfect matching on odd-degree vertices)
|
||
|
||
The first term is forced (you must traverse every edge at least once).
|
||
The second term is the minimum augmentation that makes every vertex
|
||
even-degree, which is a perfect matching on the odd-degree vertex set
|
||
because adding a matching edge is equivalent to duplicating the
|
||
shortest path between the matched pair.
|
||
|
||
BraidStorm connection: the 8-strand BraidStorm crossing graph has
|
||
8 vertices each of degree 7. The odd-degree set is the full vertex set
|
||
8, so the perfect matching has 4 edges. This prototype verifies the
|
||
minimum augmentation on a concrete crossing graph.
|
||
"""
|
||
|
||
from itertools import combinations
|
||
from typing import Dict, List, Set, Tuple
|
||
|
||
Edge = Tuple[int, int]
|
||
WeightedGraph = Dict[int, List[Tuple[int, float]]]
|
||
|
||
|
||
def all_shortest_paths(g: WeightedGraph) -> Dict[Tuple[int, int], float]:
|
||
"""Floyd–Warshall; fine for the small (≤ 8 vertex) BraidStorm case."""
|
||
n = max(g.keys()) + 1
|
||
INF = float("inf")
|
||
d = [[INF] * n for _ in range(n)]
|
||
for u in range(n):
|
||
d[u][u] = 0
|
||
for v, w in g[u]:
|
||
if w < d[u][v]:
|
||
d[u][v] = d[v][u] = w
|
||
for k in range(n):
|
||
for i in range(n):
|
||
for j in range(n):
|
||
if d[i][k] + d[k][j] < d[i][j]:
|
||
d[i][j] = d[i][k] + d[k][j]
|
||
return {(i, j): d[i][j] for i in range(n) for j in range(n)}
|
||
|
||
|
||
def minimum_weight_perfect_matching(
|
||
points: List[int], dist: Dict[Tuple[int, int], float]
|
||
) -> float:
|
||
"""Brute-force over all perfect matchings of an even-sized set.
|
||
Returns minimum sum of pairwise distances."""
|
||
if not points:
|
||
return 0.0
|
||
if len(points) == 2:
|
||
return dist[(points[0], points[1])]
|
||
first, rest = points[0], points[1:]
|
||
best = float("inf")
|
||
for i, partner in enumerate(rest):
|
||
remaining = rest[:i] + rest[i + 1:]
|
||
cost = dist[(first, partner)] + minimum_weight_perfect_matching(remaining, dist)
|
||
if cost < best:
|
||
best = cost
|
||
return best
|
||
|
||
|
||
def chinese_postman(g: WeightedGraph) -> Tuple[float, int, List[int]]:
|
||
"""Returns (total weight, augmentation edge count, odd-degree vertex list).
|
||
|
||
The augmentation edge count is the size of the odd-degree vertex
|
||
matching — i.e., the number of path duplications needed.
|
||
"""
|
||
n = max(g.keys()) + 1
|
||
total_weight = sum(w for u in range(n) for v, w in g[u] if u < v)
|
||
|
||
# Compute degree of each vertex
|
||
deg = {u: len(g[u]) for u in range(n)}
|
||
|
||
odd_vertices = sorted([u for u, d in deg.items() if d % 2 == 1])
|
||
if not odd_vertices:
|
||
return total_weight, 0, []
|
||
|
||
dist = all_shortest_paths(g)
|
||
match_cost = minimum_weight_perfect_matching(odd_vertices, dist)
|
||
return total_weight + match_cost, len(odd_vertices) // 2, odd_vertices
|
||
|
||
|
||
def braidstorm_crossing_graph() -> WeightedGraph:
|
||
"""8 vertices (strands), edge (i, j) for every distinct i ≠ j.
|
||
Weight = residual from a typical FAMM scar pattern: w(i,j) = 2^|i-j|.
|
||
|
||
Strands labeled 0..7. 8 strands × 7 crossings each = 28 edges.
|
||
Every vertex has degree 7 (odd), so the odd-degree set is all 8
|
||
vertices. The matching has 4 edges.
|
||
"""
|
||
g: WeightedGraph = {i: [] for i in range(8)}
|
||
for i in range(8):
|
||
for j in range(i + 1, 8):
|
||
w = 2 ** abs(i - j)
|
||
g[i].append((j, float(w)))
|
||
g[j].append((i, float(w)))
|
||
return g
|
||
|
||
|
||
def main() -> None:
|
||
print("=== Chinese Postman / Hierholzer-Euler demo ===\n")
|
||
g = braidstorm_crossing_graph()
|
||
total, augmentation_count, odd = chinese_postman(g)
|
||
print(f"BraidStorm crossing graph: 8 strands, 28 edges")
|
||
print(f" base edge-weight sum : {sum(w for u in range(8) for v, w in g[u] if u < v):.0f}")
|
||
print(f" odd-degree vertices : {odd}")
|
||
print(f" augmentation edge count: {augmentation_count}")
|
||
print(f" min-weight matching cost: {total - sum(w for u in range(8) for v, w in g[u] if u < v):.0f}")
|
||
print(f" total closed-walk cost : {total:.0f}")
|
||
print(f"\nEigensolid scar-pressure bound: 4 duplicated crossings, matching.")
|
||
|
||
# Smoke check: on an Eulerian graph (4-cycle), no augmentation needed
|
||
cycle4: WeightedGraph = {0: [(1, 1.0), (3, 1.0)], 1: [(0, 1.0), (2, 1.0)],
|
||
2: [(1, 1.0), (3, 1.0)], 3: [(2, 1.0), (0, 1.0)]}
|
||
total_e, aug_e, odd_e = chinese_postman(cycle4)
|
||
print(f"\n4-cycle (Eulerian): cost={total_e}, augmentation={aug_e}")
|
||
assert aug_e == 0, "Eulerian graph needs no augmentation"
|
||
print(" assertion passed: Eulerian graph needs no augmentation ✓")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|