|
75 | 75 | "MaximumClique": [Maximum Clique], |
76 | 76 | "MaximumSetPacking": [Maximum Set Packing], |
77 | 77 | "MinimumSetCovering": [Minimum Set Covering], |
| 78 | + "SetBasis": [Set Basis], |
78 | 79 | "SpinGlass": [Spin Glass], |
79 | 80 | "QUBO": [QUBO], |
80 | 81 | "ILP": [Integer Linear Programming], |
|
100 | 101 | "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], |
101 | 102 | "ShortestCommonSupersequence": [Shortest Common Supersequence], |
102 | 103 | "MinimumSumMulticenter": [Minimum Sum Multicenter], |
| 104 | + "SteinerTree": [Steiner Tree], |
103 | 105 | "SubgraphIsomorphism": [Subgraph Isomorphism], |
104 | 106 | "PartitionIntoTriangles": [Partition Into Triangles], |
105 | 107 | "FlowShopScheduling": [Flow Shop Scheduling], |
@@ -786,6 +788,72 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co |
786 | 788 | ] |
787 | 789 | ] |
788 | 790 | } |
| 791 | +#{ |
| 792 | + let x = load-model-example("SteinerTree") |
| 793 | + let nv = graph-num-vertices(x.instance) |
| 794 | + let ne = graph-num-edges(x.instance) |
| 795 | + let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) |
| 796 | + let weights = x.instance.edge_weights |
| 797 | + let terminals = x.instance.terminals |
| 798 | + let sol = x.optimal.at(0) |
| 799 | + let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) |
| 800 | + let tree-edges = tree-edge-indices.map(i => edges.at(i)) |
| 801 | + let cost = sol.metric.Valid |
| 802 | + // Steiner vertices: in tree but not terminals |
| 803 | + let tree-verts = tree-edges.map(e => (e.at(0), e.at(1))).fold((), (acc, pair) => { |
| 804 | + let (u, v) = pair |
| 805 | + let acc2 = if acc.contains(u) { acc } else { acc + (u,) } |
| 806 | + if acc2.contains(v) { acc2 } else { acc2 + (v,) } |
| 807 | + }) |
| 808 | + let steiner-verts = tree-verts.filter(v => not terminals.contains(v)) |
| 809 | + [ |
| 810 | + #problem-def("SteinerTree")[ |
| 811 | + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $T subset.eq V$ with $|T| >= 2$, find a tree $S = (V_S, E_S)$ in $G$ such that $T subset.eq V_S$, minimizing $sum_(e in E_S) w(e)$. Vertices in $V_S backslash T$ are called _Steiner vertices_. |
| 812 | + ][ |
| 813 | + One of Karp's 21 NP-complete problems @karp1972, foundational in network design with applications in telecommunications backbone routing, VLSI chip interconnect, pipeline planning, and phylogenetic tree construction. When $T = V$, the problem reduces to the minimum spanning tree (polynomial). The NP-hardness arises from choosing which Steiner vertices to include. |
| 814 | + |
| 815 | + The best known exact algorithm runs in $O^*(3^(|T|) dot n + 2^(|T|) dot n^2)$ time via Dreyfus--Wagner dynamic programming over terminal subsets @dreyfuswagner1971. Byrka _et al._ achieved a $ln(4) + epsilon approx 1.39$-approximation @byrka2013; the classic 2-approximation uses the minimum spanning tree of the terminal distance graph. |
| 816 | + |
| 817 | + // Find the unique direct terminal-terminal edge (both endpoints in T, not in the optimal tree) |
| 818 | + #let terminal-set = terminals |
| 819 | + #let direct-tt-edges = edges.enumerate().filter(((i, e)) => { |
| 820 | + terminal-set.contains(e.at(0)) and terminal-set.contains(e.at(1)) and not tree-edge-indices.contains(i) |
| 821 | + }) |
| 822 | + #let tt-edge = direct-tt-edges.at(0) |
| 823 | + #let tt-idx = tt-edge.at(0) |
| 824 | + #let tt-u = tt-edge.at(1).at(0) |
| 825 | + #let tt-v = tt-edge.at(1).at(1) |
| 826 | + |
| 827 | + *Example.* Consider $G$ with $n = #nv$ vertices, $m = #ne$ edges, and terminals $T = {#terminals.map(t => $v_#t$).join(", ")}$. The optimal Steiner tree uses edges ${#tree-edges.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")}$ with Steiner vertices ${#steiner-verts.map(v => $v_#v$).join(", ")}$ acting as relay points. The total cost is #tree-edge-indices.map(i => $#(weights.at(i))$).join($+$) $= #cost$. Note the only direct terminal--terminal edge $(v_#tt-u, v_#tt-v)$ has weight #weights.at(tt-idx), equaling the entire Steiner tree cost. |
| 828 | + |
| 829 | + #figure({ |
| 830 | + // Layout: v0 top-left, v1 top-center, v2 top-right, v3 bottom-center, v4 bottom-right |
| 831 | + let verts = ((0, 1.2), (1.2, 1.2), (2.4, 1.2), (1.2, 0), (2.4, 0)) |
| 832 | + canvas(length: 1cm, { |
| 833 | + for (idx, (u, v)) in edges.enumerate() { |
| 834 | + let on-tree = tree-edge-indices.contains(idx) |
| 835 | + g-edge(verts.at(u), verts.at(v), |
| 836 | + stroke: if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) }) |
| 837 | + let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 |
| 838 | + let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 |
| 839 | + let dx = if u == 0 and v == 3 { -0.3 } else if u == 2 and v == 3 { 0.3 } else { 0 } |
| 840 | + let dy = if u == 0 and v == 1 { 0.2 } else if u == 1 and v == 2 { 0.2 } else if u == 2 and v == 4 { 0.3 } else { 0 } |
| 841 | + draw.content((mx + dx, my + dy), text(7pt, fill: luma(80))[#weights.at(idx)]) |
| 842 | + } |
| 843 | + for (k, pos) in verts.enumerate() { |
| 844 | + let is-terminal = terminals.contains(k) |
| 845 | + g-node(pos, name: "v" + str(k), |
| 846 | + fill: if is-terminal { graph-colors.at(0) } else { white }, |
| 847 | + stroke: if is-terminal { none } else { 1pt + graph-colors.at(0) }, |
| 848 | + label: text(fill: if is-terminal { white } else { black })[$v_#k$]) |
| 849 | + } |
| 850 | + }) |
| 851 | + }, |
| 852 | + caption: [Steiner tree on #nv vertices with terminals $T = {#terminals.map(t => $v_#t$).join(", ")}$ (filled blue). Steiner vertices #steiner-verts.map(v => $v_#v$).join(", ") (outlined) relay connections. Blue edges form the optimal tree with cost #cost.], |
| 853 | + ) <fig:steiner-tree> |
| 854 | + ] |
| 855 | + ] |
| 856 | +} |
789 | 857 | #problem-def("OptimalLinearArrangement")[ |
790 | 858 | Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? |
791 | 859 | ][ |
@@ -1040,6 +1108,43 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS |
1040 | 1108 | ] |
1041 | 1109 | } |
1042 | 1110 |
|
| 1111 | +#{ |
| 1112 | + let x = load-model-example("SetBasis") |
| 1113 | + let coll = x.instance.collection |
| 1114 | + let m = coll.len() |
| 1115 | + let U-size = x.instance.universe_size |
| 1116 | + let k = x.instance.k |
| 1117 | + let sample = x.samples.at(0) |
| 1118 | + let sat-count = x.optimal.len() |
| 1119 | + let basis = range(k).map(i => |
| 1120 | + range(U-size).filter(j => sample.config.at(i * U-size + j) == 1) |
| 1121 | + ) |
| 1122 | + let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$" |
| 1123 | + [ |
| 1124 | + #problem-def("SetBasis")[ |
| 1125 | + Given finite set $S$, collection $cal(C)$ of subsets of $S$, and integer $k$, does there exist a family $cal(B) = {B_1, ..., B_k}$ with each $B_i subset.eq S$ such that for every $C in cal(C)$ there exists $cal(B)_C subset.eq cal(B)$ with $union.big_(B in cal(B)_C) B = C$? |
| 1126 | + ][ |
| 1127 | + The Set Basis problem was shown NP-complete by Stockmeyer @stockmeyer1975setbasis and appears as SP7 in Garey & Johnson @garey1979. It asks for an exact union-based description of a family of sets, unlike Set Cover which only requires covering the underlying universe. Applications include data compression, database schema design, and Boolean function minimization. The library's decision encoding uses $k |S|$ membership bits, so brute-force over those bits gives an $O^*(2^(k |S|))$ exact algorithm#footnote[This is the direct search bound induced by the encoding implemented here; we are not aware of a faster general exact worst-case algorithm for this representation.]. |
| 1128 | + |
| 1129 | + *Example.* Let $S = {1, 2, 3, 4}$, $k = #k$, and $cal(C) = {#range(m).map(i => $C_#(i + 1)$).join(", ")}$ with #coll.enumerate().map(((i, s)) => $C_#(i + 1) = #fmt-set(s)$).join(", "). The sample basis from the issue is $cal(B) = {#range(k).map(i => $B_#(i + 1)$).join(", ")}$ with #basis.enumerate().map(((i, s)) => $B_#(i + 1) = #fmt-set(s)$).join(", "). Then $C_1 = B_1 union B_2$, $C_2 = B_2 union B_3$, $C_3 = B_1 union B_3$, and $C_4 = B_1 union B_2 union B_3$. There are #sat-count satisfying encodings in total: the singleton basis can be permuted in $3! = 6$ ways, and the three pair sets $C_1, C_2, C_3$ also form a basis with another six row permutations. |
| 1130 | + |
| 1131 | + #figure( |
| 1132 | + canvas(length: 1cm, { |
| 1133 | + let elems = ((-0.9, 0.2), (0.0, -0.5), (0.9, 0.2), (1.8, -0.5)) |
| 1134 | + for i in range(k) { |
| 1135 | + let positions = basis.at(i).map(e => elems.at(e)) |
| 1136 | + sregion(positions, pad: 0.28, label: [$B_#(i + 1)$], ..sregion-selected) |
| 1137 | + } |
| 1138 | + for (idx, pos) in elems.enumerate() { |
| 1139 | + selem(pos, label: [#(idx + 1)], fill: if idx < 3 { black } else { luma(160) }) |
| 1140 | + } |
| 1141 | + }), |
| 1142 | + caption: [Set Basis example: the singleton basis $cal(B) = {#range(k).map(i => $B_#(i + 1)$).join(", ")}$ reconstructs every target set in $cal(C)$; element $4$ is unused by the target family.], |
| 1143 | + ) <fig:set-basis> |
| 1144 | + ] |
| 1145 | + ] |
| 1146 | +} |
| 1147 | + |
1043 | 1148 | == Optimization Problems |
1044 | 1149 |
|
1045 | 1150 | #{ |
|
0 commit comments