Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 0 additions & 88 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,6 @@
"ShortestCommonSuperstring": [Shortest Common Superstring],
"StaffScheduling": [Staff Scheduling],
"SteinerTree": [Steiner Tree],
"SteinerTreeInGraphs": [Steiner Tree in Graphs],
"MinimumAxiomSet": [Minimum Axiom Set],
"MinimumExternalMacroDataCompression": [Minimum External Macro Data Compression],
"MinimumInternalMacroDataCompression": [Minimum Internal Macro Data Compression],
Expand Down Expand Up @@ -3870,75 +3869,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
]
}

#{
let x = load-model-example("SteinerTreeInGraphs")
let nv = graph-num-vertices(x.instance)
let edges = x.instance.graph.edges
let ne = edges.len()
let terminals = x.instance.terminals
let weights = x.instance.edge_weights
let sol = (config: x.optimal_config, metric: x.optimal_value)
let opt-weight = metric-value(sol.metric)
// Derive tree edges from optimal config
let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i)
let tree-edges = tree-edge-indices.map(i => edges.at(i))
// Steiner vertices: non-terminal vertices that appear in tree edges
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))
[
#problem-def("SteinerTreeInGraphs")[
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)$.
][
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.

*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$.

#pred-commands(
"pred create --example SteinerTreeInGraphs -o steiner-tree-in-graphs.json",
"pred solve steiner-tree-in-graphs.json",
"pred evaluate steiner-tree-in-graphs.json --config " + cli-config(x.optimal_config),
)

#figure({
// Graph: 6 vertices arranged in two rows (layout positions)
let verts = ((0, 1), (1.5, 1), (3, 1), (1.5, -0.5), (3, -0.5), (4.5, 0.25))
canvas(length: 1cm, {
import draw: *
// Edge (0,2) idx=1 would otherwise pass straight through the collinear
// vertex $v_1$ at $(1.5, 1)$, so route it as a quadratic Bezier arc above.
let arc-ctrl = ("1": (1.5, 1.85))
for (idx, (u, v)) in edges.enumerate() {
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))
let stk = if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) }
let key = str(idx)
if key in arc-ctrl {
let c = arc-ctrl.at(key)
bezier(verts.at(u), verts.at(v), c, stroke: stk)
let mx = 0.25 * verts.at(u).at(0) + 0.5 * c.at(0) + 0.25 * verts.at(v).at(0)
let my = 0.25 * verts.at(u).at(1) + 0.5 * c.at(1) + 0.25 * verts.at(v).at(1)
draw.content((mx, my + 0.18), text(7pt, fill: luma(80))[#weights.at(idx)])
} else {
g-edge(verts.at(u), verts.at(v), stroke: stk)
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
draw.content((mx, my), text(7pt, fill: luma(80))[#weights.at(idx)])
}
}
for (k, pos) in verts.enumerate() {
let is-terminal = terminals.contains(k)
let is-steiner = steiner-verts.contains(k)
g-node(pos, name: "v" + str(k),
fill: if is-terminal { graph-colors.at(0) } else if is-steiner { luma(220) } else { white },
stroke: if is-steiner { (dash: "dashed", paint: graph-colors.at(0)) } else { 1pt + black },
label: if is-terminal { text(fill: white)[$v_#k$] } else { [$v_#k$] })
}
})
},
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.],
) <fig:steiner-tree-example>
]
]
}

#{
let x = load-model-example("MinimumSumMulticenter")
let nv = graph-num-vertices(x.instance)
Expand Down Expand Up @@ -15294,24 +15224,6 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ For each position $p$, return the unique $i$ with $x_(i,p)=1$, using the existing one-hot decoder. There are $m^2+m^3$ binary variables and $2m+3m^3+m r$ constraints, where $r$ is the number of unreachable ordered required-arc pairs; hence at most $2m+4m^3$ constraints.
]

#reduction-rule("SteinerTreeInGraphs", "ILP")[
Select edges and certify terminal connectivity by sending one unit of flow from a root terminal to every other terminal through the selected subgraph.
][
_Construction._ Fix a root terminal $r in R$. Variables: binary $y_(u,v)$ for each undirected edge $\{u,v\}$ and nonnegative flow variables $f^t_(u,v)$ on each directed edge orientation for every terminal $t in R backslash {r}$. The ILP is:
$
min quad & sum_({u,v} in E) w_(u,v) y_(u,v) \
"subject to" quad & sum_(u) f^t_(u,v) - sum_(w) f^t_(v,w) = b_(t,v) quad forall t in R backslash {r}, v in V \
& f^t_(u,v) <= y_(u,v) quad forall {u, v} in E, t in R backslash {r} \
& f^t_(v,u) <= y_(u,v) quad forall {u, v} in E, t in R backslash {r} \
& y_(u,v) in {0, 1}, f^t_(u,v) in ZZ_(>=0),
$
where $b_(t,v) = -1$ if $v = r$, $b_(t,v) = 1$ if $v = t$, and $b_(t,v) = 0$ otherwise.

_Correctness._ ($arrow.r.double$) A Steiner tree supports a unit flow from the root to every other terminal using exactly its selected edges, with the same total weight. ($arrow.l.double$) Any feasible ILP solution selects a connected subgraph spanning all terminals, and with nonnegative edge weights an optimum solution is a minimum-weight Steiner tree.

_Solution extraction._ Output the binary edge-selection vector $(y_e)_(e in E)$.
]

// Scheduling

#reduction-rule("FlowShopScheduling", "ILP")[
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ pub mod prelude {
MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement,
PartialFeedbackEdgeSet, PartitionIntoCliques, PartitionIntoPathsOfLength2,
PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman,
ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman,
UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow,
ShortestWeightConstrainedPath, TravelingSalesman, UndirectedFlowLowerBounds,
UndirectedTwoCommodityIntegralFlow,
};
pub use crate::models::misc::{
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation,
Expand Down
4 changes: 0 additions & 4 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
//! - [`PartitionIntoPathsOfLength2`]: Partition vertices into triples with at least two edges each
//! - [`PrizeCollectingSteinerForest`]: Forest minimizing omitted-prize plus edge-cost plus omega times the number of tree components
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
//! - [`SteinerTreeInGraphs`]: Minimum weight Steiner tree connecting terminal vertices
//! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem
//! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges
//! - [`BoundedComponentSpanningForest`]: Partition vertices into bounded-weight connected components
Expand Down Expand Up @@ -161,7 +160,6 @@ pub(crate) mod rural_postman;
pub(crate) mod shortest_weight_constrained_path;
pub(crate) mod spin_glass;
pub(crate) mod steiner_tree;
pub(crate) mod steiner_tree_in_graphs;
pub(crate) mod strong_connectivity_augmentation;
pub(crate) mod subgraph_isomorphism;
pub(crate) mod traveling_salesman;
Expand Down Expand Up @@ -245,7 +243,6 @@ pub use rural_postman::RuralPostman;
pub use shortest_weight_constrained_path::ShortestWeightConstrainedPath;
pub use spin_glass::SpinGlass;
pub use steiner_tree::SteinerTree;
pub use steiner_tree_in_graphs::SteinerTreeInGraphs;
pub use strong_connectivity_augmentation::StrongConnectivityAugmentation;
pub use subgraph_isomorphism::SubgraphIsomorphism;
pub use traveling_salesman::TravelingSalesman;
Expand Down Expand Up @@ -322,7 +319,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(prize_collecting_steiner_forest::canonical_model_example_specs());
specs.extend(rooted_tree_arrangement::canonical_model_example_specs());
specs.extend(steiner_tree::canonical_model_example_specs());
specs.extend(steiner_tree_in_graphs::canonical_model_example_specs());
specs.extend(directed_two_commodity_integral_flow::canonical_model_example_specs());
specs.extend(disjoint_connecting_paths::canonical_model_example_specs());
specs.extend(undirected_flow_lower_bounds::canonical_model_example_specs());
Expand Down
Loading
Loading