diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1524f98b8..4aa97f617 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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], @@ -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.], - ) - ] - ] -} - #{ let x = load-model-example("MinimumSumMulticenter") let nv = graph-num-vertices(x.instance) @@ -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")[ diff --git a/src/lib.rs b/src/lib.rs index 69381f493..ae4edab0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7835dd74f..720ca1dc6 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -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 @@ -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; @@ -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; @@ -322,7 +319,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec::FIELDS, - } -} - -/// The Steiner Tree in Graphs problem. -/// -/// Given a weighted graph G = (V, E) with edge weights w_e and a -/// subset R ⊆ V of required terminal vertices, find a subtree T of G -/// that includes all vertices of R and minimizes the total edge weight -/// Σ_{e ∈ T} w(e). -/// -/// # Representation -/// -/// Each edge is assigned a binary variable: -/// - 0: edge is not in the tree -/// - 1: edge is in the tree -/// -/// A valid Steiner tree requires: -/// - All terminal vertices are connected through selected edges -/// - Selected edges form a connected subgraph (optimally a tree) -/// -/// # Type Parameters -/// -/// * `G` - The graph type (e.g., `SimpleGraph`) -/// * `W` - The weight type for edges (e.g., `i64`, `f64`) -/// -/// # Example -/// -/// ``` -/// use problemreductions::models::graph::SteinerTreeInGraphs; -/// use problemreductions::topology::SimpleGraph; -/// use problemreductions::{Problem, BruteForce}; -/// -/// // Path graph 0-1-2-3, terminals {0, 3} -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); -/// let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1, 1, 1]); -/// -/// let solver = BruteForce::new(); -/// let solution = solver.solve(&problem).unwrap().unwrap(); -/// // Optimal: select all 3 edges (the only path from 0 to 3) -/// assert_eq!(solution, vec![true, true, true]); -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SteinerTreeInGraphs { - /// The underlying graph. - graph: G, - /// Required terminal vertices. - terminals: Vec, - /// Weights for each edge (in edge index order). - edge_weights: Vec, -} - -#[derive(Debug, Deserialize, crate::CreateSpec)] -struct SteinerTreeInGraphsCreateSpec { - /// The underlying graph. - graph: SimpleGraph, - /// Required terminal vertices. - terminals: Vec, - /// Edge weights; defaults to one per edge. - edge_weights: Option>, -} -impl TryFrom> for SteinerTreeInGraphs -where - W: WeightElement, -{ - type Error = crate::registry::ConstructionError; - fn try_from(spec: SteinerTreeInGraphsCreateSpec) -> Result { - let count = spec.graph.num_edges(); - let edge_weights = spec - .edge_weights - .unwrap_or_else(|| (0..count).map(|_| W::unit()).collect()); - if edge_weights.len() != count { - return Err(format!( - "edge_weights has {} entries, expected {count}", - edge_weights.len() - ) - .into()); - } - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!("terminal {terminal} is outside the graph").into()); - } - Ok(Self::new(spec.graph, spec.terminals, edge_weights)) - } -} - -impl SteinerTreeInGraphs { - /// Create a SteinerTreeInGraphs problem from a graph, terminals, and edge weights. - /// - /// # Panics - /// Panics if `edge_weights.len() != graph.num_edges()` or any terminal index is out of bounds. - pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - for &t in &terminals { - assert!( - t < graph.num_vertices(), - "terminal vertex {} out of bounds (num_vertices = {})", - t, - graph.num_vertices() - ); - } - Self { - graph, - terminals, - edge_weights, - } - } - - /// Get a reference to the underlying graph. - pub fn graph(&self) -> &G { - &self.graph - } - - /// Get the terminal vertices. - pub fn terminals(&self) -> &[usize] { - &self.terminals - } - - /// Get all edges with their weights. - pub fn edges(&self) -> Vec<(usize, usize, W)> { - self.graph - .edges() - .into_iter() - .zip(self.edge_weights.iter().cloned()) - .map(|((u, v), w)| (u, v, w)) - .collect() - } - - /// Set new weights for the problem. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); - self.edge_weights = weights; - } - - /// Get the weights for the problem. - pub fn weights(&self) -> Vec { - self.edge_weights.clone() - } - - /// Check if the problem uses a non-unit weight type. - pub fn is_weighted(&self) -> bool - where - W: WeightElement, - { - !W::IS_UNIT - } - - /// Check if a configuration is a valid Steiner tree. - pub fn is_valid_solution(&self, config: &[usize]) -> bool { - if config.len() != self.graph.num_edges() { - return false; - } - let selected: Vec = config.iter().map(|&s| s == 1).collect(); - is_steiner_tree(&self.graph, &self.terminals, &selected) - } -} - -impl SteinerTreeInGraphs { - /// Get the number of vertices in the underlying graph. - pub fn num_vertices(&self) -> usize { - self.graph().num_vertices() - } - - /// Get the number of edges in the underlying graph. - pub fn num_edges(&self) -> usize { - self.graph().num_edges() - } - - /// Get the number of terminal vertices. - pub fn num_terminals(&self) -> usize { - self.terminals.len() - } -} - -impl Problem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - const NAME: &'static str = "SteinerTreeInGraphs"; - type Solution = Vec; - type Value = Min; - - crate::problem_parameters![ - ("num_edges", num_edges), - ("num_terminals", num_terminals), - ("num_vertices", num_vertices), - ]; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![G, W] - } - - fn evaluate( - &self, - config: &Self::Solution, - ) -> Result, crate::traits::EvaluationError> { - Ok({ - if config.len() != self.graph.num_edges() { - return Err(crate::traits::EvaluationError::InvalidConfiguration( - "edge-selection length does not match the graph".into(), - )); - } - let selected = config; - if !is_steiner_tree(&self.graph, &self.terminals, selected) { - return Ok(Min(None)); - } - let mut total = W::Sum::zero(); - for (idx, &sel) in config.iter().enumerate() { - if sel { - if let Some(w) = self.edge_weights.get(idx) { - total = W::checked_add_to_sum( - total, - w.to_sum(), - "summing Steiner tree edge weights", - )?; - } - } - } - Min(Some(total)) - }) - } -} - -impl crate::solvers::BruteForceProblem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] - } -} - -/// Check if a selection of edges forms a valid Steiner tree (connected subgraph spanning all terminals). -/// -/// A valid Steiner tree requires: -/// 1. All terminal vertices are reachable from each other through selected edges. -/// 2. The selected edges form a connected subgraph that includes all terminals. -/// -/// Note: The optimal solution is always a tree, but we accept any connected subgraph -/// spanning all terminals (the brute-force solver will find the minimum-weight one). -/// -/// # Panics -/// Panics if `selected.len() != graph.num_edges()`. -pub(crate) fn is_steiner_tree(graph: &G, terminals: &[usize], selected: &[bool]) -> bool { - assert_eq!( - selected.len(), - graph.num_edges(), - "selected length must match num_edges" - ); - - // If no terminals, any selection is trivially valid (including empty) - if terminals.is_empty() { - return true; - } - - // If only one terminal, it's valid as long as that terminal exists - // (no edges needed to connect a single vertex) - if terminals.len() == 1 { - return true; - } - - // Build adjacency list from selected edges - let n = graph.num_vertices(); - let edges = graph.edges(); - let mut adj: Vec> = vec![vec![]; n]; - - let mut has_any_edge = false; - for (idx, &sel) in selected.iter().enumerate() { - if sel { - let (u, v) = edges[idx]; - adj[u].push(v); - adj[v].push(u); - has_any_edge = true; - } - } - - if !has_any_edge { - return false; - } - - // BFS from the first terminal to check connectivity of all terminals - let start = terminals[0]; - let mut visited = vec![false; n]; - let mut queue = std::collections::VecDeque::new(); - visited[start] = true; - queue.push_back(start); - - while let Some(node) = queue.pop_front() { - for &neighbor in &adj[node] { - if !visited[neighbor] { - visited[neighbor] = true; - queue.push_back(neighbor); - } - } - } - - // All terminals must be reachable - terminals.iter().all(|&t| visited[t]) -} - -crate::impl_random_generate!(SteinerTreeInGraphs, crate::random::SimpleGraphRandomSpec, |spec| { - if spec.num_vertices < 2 { - return Err("num_vertices must be at least 2".to_string().into()); - } - let graph = spec.graph()?; - let terminals = (0..std::cmp::max(2, spec.num_vertices / 2)).collect(); - let weights = vec![1; graph.num_edges()]; - Ok(SteinerTreeInGraphs::new(graph, terminals, weights)) -}); - -#[derive(Debug, Deserialize, crate::CreateSpec)] -struct SteinerTreeInGraphsOneCreateSpec { - /// The underlying graph. - graph: SimpleGraph, - terminals: Vec, -} - -impl TryFrom for SteinerTreeInGraphs { - type Error = crate::registry::ConstructionError; - fn try_from(spec: SteinerTreeInGraphsOneCreateSpec) -> Result { - let weights = vec![One; spec.graph.num_edges()]; - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!("terminal {terminal} is outside the graph").into()); - } - Ok(Self::new(spec.graph, spec.terminals, weights)) - } -} - -crate::declare_variants! { - default SteinerTreeInGraphs => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsCreateSpec random, - SteinerTreeInGraphs => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsOneCreateSpec, -} - -crate::register_brute_force! { - SteinerTreeInGraphs decode |_, indices: Vec| crate::config::config_to_bits(&indices), - SteinerTreeInGraphs decode |_, indices: Vec| crate::config::config_to_bits(&indices), -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "steiner_tree_in_graphs_simplegraph", - instance: Box::new(SteinerTreeInGraphs::new( - SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5)], - ), - vec![0, 3, 5], - vec![3, 2, 4, 1, 2, 3, 1], - )), - // Optimal: edges {0,2}(w=2), {2,3}(w=1), {2,5}(w=2) = weight 5 - optimal_config: serde_json::json!(vec![false, true, false, true, true, false, false]), - optimal_value: serde_json::json!(5), - }] -} - -#[cfg(test)] -#[path = "../../unit_tests/models/graph/steiner_tree_in_graphs.rs"] -mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 21220aa30..b01593468 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -43,9 +43,8 @@ pub use graph::{ PartialFeedbackEdgeSet, PartitionIntoCliques, PartitionIntoForests, PartitionIntoPathsOfLength2, PartitionIntoPerfectMatchings, PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, - SpinGlass, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, - SubgraphIsomorphism, TravelingSalesman, UndirectedFlowLowerBounds, - UndirectedTwoCommodityIntegralFlow, + SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 2b7d66b7c..86ceb7c9a 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -271,7 +271,6 @@ pub(crate) mod shortestweightconstrainedpath_ilp; pub(crate) mod sparsematrixcompression_ilp; pub(crate) mod stackercrane_ilp; pub(crate) mod steinertree_ilp; -pub(crate) mod steinertreeingraphs_ilp; pub(crate) mod stringtostringcorrection_ilp; pub(crate) mod strongconnectivityaugmentation_ilp; pub(crate) mod subgraphisomorphism_ilp; @@ -571,7 +570,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, - num_edges: usize, -} - -impl ReductionResult for ReductionSTIGToILP { - type Source = SteinerTreeInGraphs; - type Target = ILP; - - fn target_problem(&self) -> &ILP { - &self.target - } - - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok(target_solution[..self.num_edges] - .iter() - .map(|&value| value == 1) - .collect()) - } -} - -#[reduction( - transform = exact { - num_vars = "num_edges + 2 * num_edges * (num_terminals - 1)", - num_constraints = "num_vertices * (num_terminals - 1) + 2 * num_edges * (num_terminals - 1)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] -impl ReduceTo> for SteinerTreeInGraphs { - type Result = ReductionSTIGToILP; - - fn reduce_to(&self) -> Result { - if self.weights().iter().any(|&weight| weight <= 0) { - return Err(crate::rules::ReductionError::invalid_target::< - SteinerTreeInGraphs, - ILP, - >( - "ILP construction requires strictly positive edge weights" - )); - } - - let n = self.num_vertices(); - let m = self.num_edges(); - let root = *self.terminals().first().ok_or_else(|| { - crate::rules::ReductionError::invalid_target::< - SteinerTreeInGraphs, - ILP, - >("source must contain at least one terminal") - })?; - let non_root_terminals = &self.terminals()[1..]; - let edges = self.graph().edges(); - let num_vars = m + 2 * m * non_root_terminals.len(); - let mut constraints = Vec::new(); - - let edge_var = |edge_idx: usize| edge_idx; - let flow_var = |terminal_pos: usize, edge_idx: usize, dir: usize| -> usize { - m + terminal_pos * 2 * m + 2 * edge_idx + dir - }; - - // Flow conservation for each non-root terminal commodity - for (terminal_pos, &terminal) in non_root_terminals.iter().enumerate() { - for vertex in 0..n { - let mut terms = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if v == vertex { - terms.push((flow_var(terminal_pos, edge_idx, 0), 1)); - terms.push((flow_var(terminal_pos, edge_idx, 1), -1)); - } - if u == vertex { - terms.push((flow_var(terminal_pos, edge_idx, 0), -1)); - terms.push((flow_var(terminal_pos, edge_idx, 1), 1)); - } - } - - let rhs = if vertex == root { - -1 - } else if vertex == terminal { - 1 - } else { - 0 - }; - constraints.push(LinearConstraint::eq(terms, rhs)); - } - } - - // Capacity linking: f^t_{e,dir} <= y_e - for terminal_pos in 0..non_root_terminals.len() { - for edge_idx in 0..m { - let selector = edge_var(edge_idx); - constraints.push(LinearConstraint::le( - vec![(flow_var(terminal_pos, edge_idx, 0), 1), (selector, -1)], - 0, - )); - constraints.push(LinearConstraint::le( - vec![(flow_var(terminal_pos, edge_idx, 1), 1), (selector, -1)], - 0, - )); - } - } - - // Objective: minimize total weight - let edge_weights = self.weights(); - let objective: Vec<(usize, i64)> = edge_weights - .iter() - .enumerate() - .map(|(edge_idx, weight)| (edge_var(edge_idx), weight.to_sum())) - .collect(); - - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; - - Ok(ReductionSTIGToILP { - target, - num_edges: m, - }) - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "steinertreeingraphs_to_ilp", - build: || { - // 4 vertices, 4 edges, 2 terminals - // ILP: 4 + 2*4*1 = 12 binary variables = 4096 configs - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), - vec![0, 2], - vec![1, 1, 1, 3], - ); - crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/steinertreeingraphs_ilp.rs"] -mod tests; diff --git a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs deleted file mode 100644 index fd845b214..000000000 --- a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs +++ /dev/null @@ -1,232 +0,0 @@ -use super::*; -use crate::solvers::BruteForceProblem as _; - -#[test] -fn create_spec_defaults_edge_weights() { - let p = SteinerTreeInGraphs::try_from(SteinerTreeInGraphsCreateSpec:: { - graph: SimpleGraph::new(2, vec![(0, 1)]), - terminals: vec![0, 1], - edge_weights: None, - }) - .unwrap(); - assert_eq!(p.weights(), &[1]); -} -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::Problem; - -#[test] -fn test_steiner_tree_creation() { - // Path graph: 0-1-2-3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1i64, 2, 3]); - assert_eq!(problem.graph().num_vertices(), 4); - assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.terminals(), &[0, 3]); - assert_eq!(problem.dimensions().len(), 3); - assert_eq!(problem.num_vertices(), 4); - assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_terminals(), 2); -} - -#[test] -fn test_steiner_tree_evaluation() { - // Triangle graph: 0-1, 1-2, 0-2, with terminal {0, 2} - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![3i64, 4, 1]); - - // Select edge 0-2 (weight 1): valid, connects terminals directly - let config_direct = vec![false, false, true]; - let result = problem.evaluate(&config_direct).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 1); - - // Select edges 0-1 and 1-2 (weights 3+4=7): valid, connects via vertex 1 - let config_via = vec![true, true, false]; - let result = problem.evaluate(&config_via).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 7); - - // Select only edge 0-1: invalid (terminal 2 not reached) - let config_invalid = vec![true, false, false]; - let result = problem.evaluate(&config_invalid).unwrap(); - assert!(!result.is_valid()); - - // Select no edges: invalid - let config_empty = vec![false, false, false]; - let result = problem.evaluate(&config_empty).unwrap(); - assert!(!result.is_valid()); -} - -#[test] -fn test_steiner_tree_solver() { - // Diamond graph: - // 1 - // / \ - // 0 3 - // \ / - // 2 - // Edges: 0-1(w=2), 0-2(w=1), 1-3(w=2), 2-3(w=1) - // Terminals: {0, 3} - // Optimal path: 0-2-3 with weight 1+1=2 - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![2, 1, 2, 1]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 2); - // Should select edges 0-2 and 2-3 - assert_eq!(solution, vec![false, true, false, true]); -} - -#[test] -fn test_steiner_tree_with_steiner_vertices() { - // Star graph: center vertex 1 connected to 0, 2, 3 - // Edges: 0-1(w=1), 1-2(w=1), 1-3(w=1) - // Terminals: {0, 2, 3} - // Optimal: use vertex 1 as Steiner vertex, select all 3 edges, weight = 3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (1, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 3], vec![1i64; 3]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 3); - assert_eq!(solution, vec![true, true, true]); -} - -#[test] -fn test_steiner_tree_is_valid_solution() { - // Path graph: 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - - // Valid: both edges selected - assert!(problem.is_valid_solution(&[1, 1])); - // Invalid: only first edge - assert!(!problem.is_valid_solution(&[1, 0])); - // Invalid: only second edge - assert!(!problem.is_valid_solution(&[0, 1])); - // Invalid: no edges - assert!(!problem.is_valid_solution(&[0, 0])); -} - -#[test] -fn test_steiner_tree_parameter_getters() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 4], vec![1i64; 4]); - assert_eq!(problem.num_vertices(), 5); - assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.num_terminals(), 3); -} - -#[test] -fn test_steiner_tree_problem_name() { - assert_eq!( - as Problem>::NAME, - "SteinerTreeInGraphs" - ); -} - -#[test] -fn test_steiner_tree_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - let json = serde_json::to_string(&problem).unwrap(); - let deserialized: SteinerTreeInGraphs = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.graph().num_vertices(), 3); - assert_eq!(deserialized.terminals(), &[0, 2]); - assert_eq!(deserialized.num_edges(), 2); -} - -#[test] -fn test_steiner_tree_single_terminal() { - // Single terminal: any config (including empty) is valid - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![1], vec![1i64; 2]); - - // No edges needed for a single terminal - let result = problem.evaluate(&vec![false, false]).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 0); -} - -#[test] -fn test_steiner_tree_all_vertices_terminal() { - // When all vertices are terminals, it degenerates to spanning tree - // Path: 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 1, 2], vec![1i64; 2]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 2); -} - -#[test] -fn test_steiner_tree_edges_accessor() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![5i64, 10]); - let edges = problem.edges(); - assert_eq!(edges.len(), 2); - assert_eq!(edges[0].2, 5); - assert_eq!(edges[1].2, 10); -} - -#[test] -fn test_steiner_tree_weights_management() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let mut problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - assert!(problem.is_weighted()); - assert_eq!(problem.weights(), vec![1, 1]); - - problem.set_weights(vec![5, 10]); - assert_eq!(problem.weights(), vec![5, 10]); -} - -#[test] -fn test_steiner_tree_example_from_issue() { - // Example from issue #255: - // Graph with 8 vertices {0,1,2,3,4,5,6,7} and 12 edges - // Terminals R = {0, 3, 5, 7} - let graph = SimpleGraph::new( - 8, - vec![ - (0, 1), // w=2, idx=0 - (0, 2), // w=3, idx=1 - (1, 2), // w=1, idx=2 - (1, 3), // w=4, idx=3 - (2, 4), // w=2, idx=4 - (3, 4), // w=3, idx=5 - (3, 5), // w=5, idx=6 - (4, 5), // w=1, idx=7 - (4, 6), // w=2, idx=8 - (5, 6), // w=3, idx=9 - (5, 7), // w=4, idx=10 - (6, 7), // w=1, idx=11 - ], - ); - let weights = vec![2, 3, 1, 4, 2, 3, 5, 1, 2, 3, 4, 1]; - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3, 5, 7], weights); - - // Brute-force verification: independently confirm optimal weight is 12 - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 12); - - // Verify the claimed optimal solution from the issue: - // Edges: {0,1}(2) + {1,2}(1) + {2,4}(2) + {3,4}(3) + {4,5}(1) + {4,6}(2) + {6,7}(1) = 12 - let config = vec![ - true, false, true, false, true, true, false, true, true, false, false, true, - ]; - let result = problem.evaluate(&config).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 12); -} diff --git a/src/unit_tests/registry/variant.rs b/src/unit_tests/registry/variant.rs index eb909bf25..80b246964 100644 --- a/src/unit_tests/registry/variant.rs +++ b/src/unit_tests/registry/variant.rs @@ -320,7 +320,7 @@ fn established_random_generation_models_remain_registered() { DecisionMinimumVertexCover MaximumIndependentSet MinimumVertexCover MaximumClique MinimumDominatingSet MaximalIS KClique MinimumCutIntoBoundedSets HamiltonianCircuit HamiltonianPath HamiltonianPathBetweenTwoVertices LongestCircuit MinimumMaximalMatching - RootedTreeArrangement SteinerTree SteinerTreeInGraphs LengthBoundedDisjointPaths + RootedTreeArrangement SteinerTree LengthBoundedDisjointPaths MaximumAchromaticNumber MaximumDomaticNumber MinimumCoveringByCliques MinimumIntersectionGraphBasis MaximumLeafSpanningTree GeneralizedHex BottleneckTravelingSalesman MaxCut MaximumMatching TravelingSalesman SpinGlass KColoring @@ -367,7 +367,7 @@ fn unit_variants_construct_without_unit_inputs() { "MaximumCoKPlex" => json!({"graph":graph,"k":1}), "MinimumFeedbackVertexSet" => json!({"graph":{"num_vertices":3,"arcs":[[0,1],[1,2]]}}), "MaximumSetPacking" => json!({"subsets":[[0,1],[1,2]]}), - "SteinerTree" | "SteinerTreeInGraphs" => json!({"graph":graph,"terminals":[0,2]}), + "SteinerTree" => json!({"graph":graph,"terminals":[0,2]}), "MaximumIndependentSet" => match entry.variant_map()["graph"].as_str() { "SimpleGraph" => json!({"graph":[[0,1],[1,2]]}), "KingsSubgraph" => json!({"positions":[[0,0],[1,0],[2,0]]}), @@ -438,10 +438,6 @@ fn unit_construction_preserves_model_validation() { ("SteinerTree", json!({"graph":graph,"terminals":[0]})), ("SteinerTree", json!({"graph":graph,"terminals":[0,0]})), ("SteinerTree", json!({"graph":graph,"terminals":[0,3]})), - ( - "SteinerTreeInGraphs", - json!({"graph":graph,"terminals":[3]}), - ), ( "MinimumTardinessSequencing", json!({"deadlines":[1,2],"precedences":[[0,2]]}), diff --git a/src/unit_tests/rules/steinertreeingraphs_ilp.rs b/src/unit_tests/rules/steinertreeingraphs_ilp.rs deleted file mode 100644 index d09885b41..000000000 --- a/src/unit_tests/rules/steinertreeingraphs_ilp.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::*; -use crate::models::algebraic::ILP; -use crate::rules::test_helpers::assert_bf_vs_ilp; -use crate::rules::ReduceTo; -use crate::topology::SimpleGraph; - -#[test] -fn test_steinertreeingraphs_to_ilp_closed_loop() { - // Path graph: 0 - 1 - 2, terminals {0, 2}, weights [1, 1] - // Optimal Steiner tree: use both edges (cost 2) - // ILP variables: 2 + 2*2*1 = 6 binary = 64 configs - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), - vec![0, 2], - vec![1, 1], - ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_steinertreeingraphs_to_ilp_bf_vs_ilp() { - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), - vec![0, 2], - vec![1, 1], - ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); -}