graph LR G["Primal graph\nG = (V, E)"] --> EMB["Planar\nembedding"] EMB --> FACES["Faces\n(inner + outer)"] FACES --> DUAL["Dual graph\nG*"] DUAL --> ODD["Odd faces\n|f| odd โ T"] ODD --> MWPM["Min-weight perfect\nmatching on T"] MWPM --> EXCL["Excluded\nprimal edges"] EXCL --> CUT["Max cut\nE \ excluded"] style G fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style FACES fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px style DUAL fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px style ODD fill:#fff9c4,stroke:#f57f17,stroke-width:3px style MWPM fill:#f8bbd0,stroke:#c2185b,stroke-width:3px style CUT fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
Where 5.6 s of solve_hadlock_max_cut went โ tri(14,14) min_weight_matching 5.165 s ยท 92% all_pairs_dijkstra_path 0.220 s ยท 4% all_pairs_dijkstra_path_length 0.166 s ยท 3% build metric closure 0.072 s ยท 1% planarity + faces + dual 0.020 s min_weight_matching is pure-Python blossom on a complete graph with k = 196 nodes.
graph LR PLAN["check_planarity\n0.015 s"] --> FACE["find_faces\n0.002 s"] FACE --> DUAL["build_dual\n0.003 s"] DUAL --> SP["all-pairs Dijkstra\n0.386 s ยท 7%"] SP --> MATCH["min_weight_matching\n5.165 s ยท 92%"] MATCH --> WALK["walk matched paths"] WALK --> CUT["cut = E \ excluded"] style PLAN fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style FACE fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style DUAL fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style SP fill:#fff9c4,stroke:#f57f17,stroke-width:3px style MATCH fill:#bf616a,stroke:#8b2c3a,stroke-width:4px,color:#ffffff style CUT fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
End-to-end after touching everything except the matcher baseline 5.64 s all the obvious fixes 5.13 s ยท 1.1ร The shortest-path side is 7% of the runtime. Halving it changes the total by ~3%. The matcher is the wall.
graph LR PY["netlistx\nPython reference"] CPP["xnetwork-cpp\nhadlock.hpp"] BLOSSOM["detail/blossom.hpp\nadapter"] MW["mwmatching.hpp\nJoris van Rantwijk, MIT"] PY -.->|"same algorithm"| CPP CPP --> BLOSSOM BLOSSOM --> MW style PY fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style CPP fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style BLOSSOM fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px style MW fill:#ffe0b2,stroke:#e65100,stroke-width:3px
Matching k = 196 on the real tri(14,14) metric closure networkx (Python) 5 200 ms xnetwork-cpp (MSVC /O2) 21.1 ms ยท 246ร Same blossom, same result โ the win is purely compilation.
graph LR POS["odd_faces = [0..91, 93..100]\nouter face sits at index 92"] POS --> SHIFT["faces after 92 are shifted\nby one position โ"] POS --> OOB["dist_mat[100] with size 100\nโ out of bounds ๐ฅ"] SHIFT --> BAD["corrupted metric\nโ suboptimal cut"] OOB --> BAD style POS fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style SHIFT fill:#f8bbd0,stroke:#c2185b,stroke-width:3px style OOB fill:#f8bbd0,stroke:#c2185b,stroke-width:3px style BAD fill:#bf616a,stroke:#8b2c3a,stroke-width:4px,color:#ffffff
graph LR F1["1. dist_mat keyed by face id\n(correctness)"] F2["2. throw on odd parity\n(was a silent pop_back)"] F3["3. skip bridge self-loops\nin build_dual"] F1 --> F2 --> F3 --> N(( )) style F1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style F2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px style F3 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
graph LR F4["4. drop the O(kยฒ) path table\nreconstruct lazily"] F5["5. std::map โ unordered_map\n+ reserve"] F6["6. flat is_odd array\n(no hash lookup)"] N(( )) --> F4 --> F5 --> F6 style F4 fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style F5 fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style F6 fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
Growth: bitmask DP vs blossom bitmask DP ยท 2^k k = 160 ๐ฅ blossom ยท kยณ milliseconds โ The fix: use the mwmatching crate already in Cargo.toml.
graph LR CYC["Start from a cycle\nโ embedded subgraph H"] --> FRAG["Find fragments\nof G โ H"] FRAG --> ADM["Admissible faces\n= faces containing\nall attachments"] ADM -->|"none"| NONPLAN["NOT PLANAR\nreturn None โ"] ADM -->|"โฅ1"| PICK["Pick forced fragment\nif unique, else any"] PICK --> PATH["Embed a path between\nattachments in that face"] PATH --> DONE{"H = G ?"} DONE -->|"no"| FRAG DONE -->|"yes"| FACES["Return all faces โ "] style CYC fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style FRAG fill:#d1c4e9,stroke:#6a1b9a,stroke-width:3px style ADM fill:#fff9c4,stroke:#f57f17,stroke-width:3px style NONPLAN fill:#f8bbd0,stroke:#c2185b,stroke-width:3px style PATH fill:#e3f2fd,stroke:#1565c0,stroke-width:3px style FACES fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
Max-cut weight ยท triangulated grid, unit weights 3ร3 ยท 16 nodes 24 = 24 โ 6ร6 ยท 49 nodes 84 = 84 โ 10ร10 ยท 121 nodes 220 = 220 โ The 10ร10 case has ~160 odd faces โ the old 2^k matcher could never reach it.
End-to-end solve time (log-ish, same instance) Python ๐ 5.64 s C++ (old) โ๏ธ 51.0 ms ยท suboptimal โ C++ (new) โ๏ธ 29.8 ms ยท exact โ Rust ๐ฆ now runs at all โ (was 2^160)