|
116 | 116 | "Partition": [Partition], |
117 | 117 | "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], |
118 | 118 | "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], |
| 119 | + "SteinerTreeInGraphs": [Steiner Tree in Graphs], |
119 | 120 | "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], |
120 | 121 | "MultipleChoiceBranching": [Multiple Choice Branching], |
121 | 122 | "PartitionIntoPathsOfLength2": [Partition into Paths of Length 2], |
@@ -1518,6 +1519,58 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], |
1518 | 1519 | *Example.* Consider the graph $G$ with $n = 9$ vertices and edges ${0,1}, {1,2}, {3,4}, {4,5}, {6,7}, {7,8}$ (plus cross-edges ${0,3}, {2,5}, {3,6}, {5,8}$). Setting $q = 3$, the partition $V_1 = {0,1,2}$, $V_2 = {3,4,5}$, $V_3 = {6,7,8}$ is valid: $V_1$ contains edges ${0,1}, {1,2}$ (path $0 dash.em 1 dash.em 2$), $V_2$ contains ${3,4}, {4,5}$, and $V_3$ contains ${6,7}, {7,8}$. |
1519 | 1520 | ] |
1520 | 1521 |
|
| 1522 | +#{ |
| 1523 | + let x = load-model-example("SteinerTreeInGraphs") |
| 1524 | + let nv = graph-num-vertices(x.instance) |
| 1525 | + let edges = x.instance.graph.edges |
| 1526 | + let ne = edges.len() |
| 1527 | + let terminals = x.instance.terminals |
| 1528 | + let weights = x.instance.edge_weights |
| 1529 | + let sol = (config: x.optimal_config, metric: x.optimal_value) |
| 1530 | + let opt-weight = sol.metric.Valid |
| 1531 | + // Derive tree edges from optimal config |
| 1532 | + let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) |
| 1533 | + let tree-edges = tree-edge-indices.map(i => edges.at(i)) |
| 1534 | + // Steiner vertices: non-terminal vertices that appear in tree edges |
| 1535 | + let steiner-verts = range(nv).filter(v => not terminals.contains(v) and tree-edges.any(e => e.at(0) == v or e.at(1) == v)) |
| 1536 | + [ |
| 1537 | + #problem-def("SteinerTreeInGraphs")[ |
| 1538 | + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $R subset.eq V$, find a subtree $T$ of $G$ that spans all terminals in $R$ and minimizes the total edge weight $sum_(e in T) w(e)$. |
| 1539 | + ][ |
| 1540 | + A classical NP-complete problem from Karp's list (as "Steiner Tree in Graphs," Garey & Johnson ND12) @karp1972. Central to network design, VLSI layout, and phylogenetic reconstruction. The problem generalizes minimum spanning tree (where $R = V$) and shortest path (where $|R| = 2$). The Dreyfus--Wagner dynamic programming algorithm @dreyfuswagner1971 solves it in $O(3^k dot n + 2^k dot n^2 + n^3)$ time, where $k = |R|$ and $n = |V|$. Bjorklund et al. @bjorklund2007 achieved $O^*(2^k)$ using subset convolution over the Mobius algebra, and Nederlof @nederlof2009 gave an $O^*(2^k)$ polynomial-space algorithm. |
| 1541 | + |
| 1542 | + *Example.* Consider a graph $G$ with $n = #nv$ vertices and $|E| = #ne$ edges. The terminals are $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue). The optimal Steiner tree uses Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (gray, dashed border) and edges #tree-edges.map(e => [$\{v_#(e.at(0)), v_#(e.at(1))\}$]).join(", ") with total weight #tree-edge-indices.map(i => str(weights.at(i))).join(" + ") $= #opt-weight$. |
| 1543 | + |
| 1544 | + #figure({ |
| 1545 | + // Graph: 6 vertices arranged in two rows (layout positions) |
| 1546 | + let verts = ((0, 1), (1.5, 1), (3, 1), (1.5, -0.5), (3, -0.5), (4.5, 0.25)) |
| 1547 | + canvas(length: 1cm, { |
| 1548 | + // Draw edges |
| 1549 | + for (idx, (u, v)) in edges.enumerate() { |
| 1550 | + let on-tree = tree-edges.any(t => (t.at(0) == u and t.at(1) == v) or (t.at(0) == v and t.at(1) == u)) |
| 1551 | + g-edge(verts.at(u), verts.at(v), |
| 1552 | + stroke: if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) }) |
| 1553 | + let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 |
| 1554 | + let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 |
| 1555 | + draw.content((mx, my), text(7pt, fill: luma(80))[#weights.at(idx)]) |
| 1556 | + } |
| 1557 | + // Draw vertices |
| 1558 | + for (k, pos) in verts.enumerate() { |
| 1559 | + let is-terminal = terminals.contains(k) |
| 1560 | + let is-steiner = steiner-verts.contains(k) |
| 1561 | + g-node(pos, name: "v" + str(k), |
| 1562 | + fill: if is-terminal { graph-colors.at(0) } else if is-steiner { luma(220) } else { white }, |
| 1563 | + stroke: if is-steiner { (dash: "dashed", paint: graph-colors.at(0)) } else { 1pt + black }, |
| 1564 | + label: if is-terminal { text(fill: white)[$v_#k$] } else { [$v_#k$] }) |
| 1565 | + } |
| 1566 | + }) |
| 1567 | + }, |
| 1568 | + caption: [Steiner Tree: terminals $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue), Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (dashed). Optimal tree (blue edges) has weight #opt-weight.], |
| 1569 | + ) <fig:steiner-tree-example> |
| 1570 | + ] |
| 1571 | + ] |
| 1572 | +} |
| 1573 | + |
1521 | 1574 | #{ |
1522 | 1575 | let x = load-model-example("MinimumSumMulticenter") |
1523 | 1576 | let nv = graph-num-vertices(x.instance) |
|
0 commit comments