Skip to main content

problemreductions/models/graph/
steiner_tree_in_graphs.rs

1//! Steiner Tree in Graphs problem implementation.
2//!
3//! The Steiner Tree problem asks for a minimum-weight subtree of a graph
4//! that connects all terminal vertices.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::{Min, One, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "SteinerTreeInGraphs",
16        display_name: "Steiner Tree in Graphs",
17        aliases: &[],
18        dimensions: &[
19            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
20            VariantDimension::new("weight", "i64", &["One", "i64"]),
21        ],
22        category: crate::registry::ProblemCategory::Graph,
23        module_path: module_path!(),
24        description: "Find minimum weight subtree connecting all terminal vertices",
25        fields: SteinerTreeInGraphsCreateSpec::<i64>::FIELDS,
26    }
27}
28
29/// The Steiner Tree in Graphs problem.
30///
31/// Given a weighted graph G = (V, E) with edge weights w_e and a
32/// subset R ⊆ V of required terminal vertices, find a subtree T of G
33/// that includes all vertices of R and minimizes the total edge weight
34/// Σ_{e ∈ T} w(e).
35///
36/// # Representation
37///
38/// Each edge is assigned a binary variable:
39/// - 0: edge is not in the tree
40/// - 1: edge is in the tree
41///
42/// A valid Steiner tree requires:
43/// - All terminal vertices are connected through selected edges
44/// - Selected edges form a connected subgraph (optimally a tree)
45///
46/// # Type Parameters
47///
48/// * `G` - The graph type (e.g., `SimpleGraph`)
49/// * `W` - The weight type for edges (e.g., `i64`, `f64`)
50///
51/// # Example
52///
53/// ```
54/// use problemreductions::models::graph::SteinerTreeInGraphs;
55/// use problemreductions::topology::SimpleGraph;
56/// use problemreductions::{Problem, BruteForce};
57///
58/// // Path graph 0-1-2-3, terminals {0, 3}
59/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]);
60/// let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1, 1, 1]);
61///
62/// let solver = BruteForce::new();
63/// let solution = solver.solve(&problem).unwrap().unwrap();
64/// // Optimal: select all 3 edges (the only path from 0 to 3)
65/// assert_eq!(solution, vec![true, true, true]);
66/// ```
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SteinerTreeInGraphs<G, W> {
69    /// The underlying graph.
70    graph: G,
71    /// Required terminal vertices.
72    terminals: Vec<usize>,
73    /// Weights for each edge (in edge index order).
74    edge_weights: Vec<W>,
75}
76
77#[derive(Debug, Deserialize, crate::CreateSpec)]
78struct SteinerTreeInGraphsCreateSpec<W> {
79    /// The underlying graph.
80    graph: SimpleGraph,
81    /// Required terminal vertices.
82    terminals: Vec<usize>,
83    /// Edge weights; defaults to one per edge.
84    edge_weights: Option<Vec<W>>,
85}
86impl<W> TryFrom<SteinerTreeInGraphsCreateSpec<W>> for SteinerTreeInGraphs<SimpleGraph, W>
87where
88    W: WeightElement,
89{
90    type Error = crate::registry::ConstructionError;
91    fn try_from(spec: SteinerTreeInGraphsCreateSpec<W>) -> Result<Self, Self::Error> {
92        let count = spec.graph.num_edges();
93        let edge_weights = spec
94            .edge_weights
95            .unwrap_or_else(|| (0..count).map(|_| W::unit()).collect());
96        if edge_weights.len() != count {
97            return Err(format!(
98                "edge_weights has {} entries, expected {count}",
99                edge_weights.len()
100            )
101            .into());
102        }
103        if let Some(&terminal) = spec
104            .terminals
105            .iter()
106            .find(|&&t| t >= spec.graph.num_vertices())
107        {
108            return Err(format!("terminal {terminal} is outside the graph").into());
109        }
110        Ok(Self::new(spec.graph, spec.terminals, edge_weights))
111    }
112}
113
114impl<G: Graph, W: Clone + Default> SteinerTreeInGraphs<G, W> {
115    /// Create a SteinerTreeInGraphs problem from a graph, terminals, and edge weights.
116    ///
117    /// # Panics
118    /// Panics if `edge_weights.len() != graph.num_edges()` or any terminal index is out of bounds.
119    pub fn new(graph: G, terminals: Vec<usize>, edge_weights: Vec<W>) -> Self {
120        assert_eq!(
121            edge_weights.len(),
122            graph.num_edges(),
123            "edge_weights length must match num_edges"
124        );
125        for &t in &terminals {
126            assert!(
127                t < graph.num_vertices(),
128                "terminal vertex {} out of bounds (num_vertices = {})",
129                t,
130                graph.num_vertices()
131            );
132        }
133        Self {
134            graph,
135            terminals,
136            edge_weights,
137        }
138    }
139
140    /// Get a reference to the underlying graph.
141    pub fn graph(&self) -> &G {
142        &self.graph
143    }
144
145    /// Get the terminal vertices.
146    pub fn terminals(&self) -> &[usize] {
147        &self.terminals
148    }
149
150    /// Get all edges with their weights.
151    pub fn edges(&self) -> Vec<(usize, usize, W)> {
152        self.graph
153            .edges()
154            .into_iter()
155            .zip(self.edge_weights.iter().cloned())
156            .map(|((u, v), w)| (u, v, w))
157            .collect()
158    }
159
160    /// Set new weights for the problem.
161    pub fn set_weights(&mut self, weights: Vec<W>) {
162        assert_eq!(weights.len(), self.graph.num_edges());
163        self.edge_weights = weights;
164    }
165
166    /// Get the weights for the problem.
167    pub fn weights(&self) -> Vec<W> {
168        self.edge_weights.clone()
169    }
170
171    /// Check if the problem uses a non-unit weight type.
172    pub fn is_weighted(&self) -> bool
173    where
174        W: WeightElement,
175    {
176        !W::IS_UNIT
177    }
178
179    /// Check if a configuration is a valid Steiner tree.
180    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
181        if config.len() != self.graph.num_edges() {
182            return false;
183        }
184        let selected: Vec<bool> = config.iter().map(|&s| s == 1).collect();
185        is_steiner_tree(&self.graph, &self.terminals, &selected)
186    }
187}
188
189impl<G: Graph, W: WeightElement> SteinerTreeInGraphs<G, W> {
190    /// Get the number of vertices in the underlying graph.
191    pub fn num_vertices(&self) -> usize {
192        self.graph().num_vertices()
193    }
194
195    /// Get the number of edges in the underlying graph.
196    pub fn num_edges(&self) -> usize {
197        self.graph().num_edges()
198    }
199
200    /// Get the number of terminal vertices.
201    pub fn num_terminals(&self) -> usize {
202        self.terminals.len()
203    }
204}
205
206impl<G, W> Problem for SteinerTreeInGraphs<G, W>
207where
208    G: Graph + crate::variant::VariantParam,
209    W: WeightElement + crate::variant::VariantParam,
210{
211    const NAME: &'static str = "SteinerTreeInGraphs";
212    type Solution = Vec<bool>;
213    type Value = Min<W::Sum>;
214
215    crate::problem_parameters![
216        ("num_edges", num_edges),
217        ("num_terminals", num_terminals),
218        ("num_vertices", num_vertices),
219    ];
220
221    fn variant() -> Vec<(&'static str, &'static str)> {
222        crate::variant_params![G, W]
223    }
224
225    fn evaluate(
226        &self,
227        config: &Self::Solution,
228    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
229        Ok({
230            if config.len() != self.graph.num_edges() {
231                return Err(crate::traits::EvaluationError::InvalidConfiguration(
232                    "edge-selection length does not match the graph".into(),
233                ));
234            }
235            let selected = config;
236            if !is_steiner_tree(&self.graph, &self.terminals, selected) {
237                return Ok(Min(None));
238            }
239            let mut total = W::Sum::zero();
240            for (idx, &sel) in config.iter().enumerate() {
241                if sel {
242                    if let Some(w) = self.edge_weights.get(idx) {
243                        total = W::checked_add_to_sum(
244                            total,
245                            w.to_sum(),
246                            "summing Steiner tree edge weights",
247                        )?;
248                    }
249                }
250            }
251            Min(Some(total))
252        })
253    }
254}
255
256impl<G, W> crate::solvers::BruteForceProblem for SteinerTreeInGraphs<G, W>
257where
258    G: Graph + crate::variant::VariantParam,
259    W: WeightElement + crate::variant::VariantParam,
260{
261    fn dimensions(&self) -> Vec<usize> {
262        vec![2; self.graph.num_edges()]
263    }
264}
265
266/// Check if a selection of edges forms a valid Steiner tree (connected subgraph spanning all terminals).
267///
268/// A valid Steiner tree requires:
269/// 1. All terminal vertices are reachable from each other through selected edges.
270/// 2. The selected edges form a connected subgraph that includes all terminals.
271///
272/// Note: The optimal solution is always a tree, but we accept any connected subgraph
273/// spanning all terminals (the brute-force solver will find the minimum-weight one).
274///
275/// # Panics
276/// Panics if `selected.len() != graph.num_edges()`.
277pub(crate) fn is_steiner_tree<G: Graph>(graph: &G, terminals: &[usize], selected: &[bool]) -> bool {
278    assert_eq!(
279        selected.len(),
280        graph.num_edges(),
281        "selected length must match num_edges"
282    );
283
284    // If no terminals, any selection is trivially valid (including empty)
285    if terminals.is_empty() {
286        return true;
287    }
288
289    // If only one terminal, it's valid as long as that terminal exists
290    // (no edges needed to connect a single vertex)
291    if terminals.len() == 1 {
292        return true;
293    }
294
295    // Build adjacency list from selected edges
296    let n = graph.num_vertices();
297    let edges = graph.edges();
298    let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
299
300    let mut has_any_edge = false;
301    for (idx, &sel) in selected.iter().enumerate() {
302        if sel {
303            let (u, v) = edges[idx];
304            adj[u].push(v);
305            adj[v].push(u);
306            has_any_edge = true;
307        }
308    }
309
310    if !has_any_edge {
311        return false;
312    }
313
314    // BFS from the first terminal to check connectivity of all terminals
315    let start = terminals[0];
316    let mut visited = vec![false; n];
317    let mut queue = std::collections::VecDeque::new();
318    visited[start] = true;
319    queue.push_back(start);
320
321    while let Some(node) = queue.pop_front() {
322        for &neighbor in &adj[node] {
323            if !visited[neighbor] {
324                visited[neighbor] = true;
325                queue.push_back(neighbor);
326            }
327        }
328    }
329
330    // All terminals must be reachable
331    terminals.iter().all(|&t| visited[t])
332}
333
334crate::impl_random_generate!(SteinerTreeInGraphs<SimpleGraph, i64>, crate::random::SimpleGraphRandomSpec, |spec| {
335    if spec.num_vertices < 2 {
336        return Err("num_vertices must be at least 2".to_string().into());
337    }
338    let graph = spec.graph()?;
339    let terminals = (0..std::cmp::max(2, spec.num_vertices / 2)).collect();
340    let weights = vec![1; graph.num_edges()];
341    Ok(SteinerTreeInGraphs::new(graph, terminals, weights))
342});
343
344#[derive(Debug, Deserialize, crate::CreateSpec)]
345struct SteinerTreeInGraphsOneCreateSpec {
346    /// The underlying graph.
347    graph: SimpleGraph,
348    terminals: Vec<usize>,
349}
350
351impl TryFrom<SteinerTreeInGraphsOneCreateSpec> for SteinerTreeInGraphs<SimpleGraph, One> {
352    type Error = crate::registry::ConstructionError;
353    fn try_from(spec: SteinerTreeInGraphsOneCreateSpec) -> Result<Self, Self::Error> {
354        let weights = vec![One; spec.graph.num_edges()];
355        if let Some(&terminal) = spec
356            .terminals
357            .iter()
358            .find(|&&t| t >= spec.graph.num_vertices())
359        {
360            return Err(format!("terminal {terminal} is outside the graph").into());
361        }
362        Ok(Self::new(spec.graph, spec.terminals, weights))
363    }
364}
365
366crate::declare_variants! {
367    default SteinerTreeInGraphs<SimpleGraph, i64> => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsCreateSpec<i64> random,
368    SteinerTreeInGraphs<SimpleGraph, One> => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsOneCreateSpec,
369}
370
371crate::register_brute_force! {
372    SteinerTreeInGraphs<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
373    SteinerTreeInGraphs<SimpleGraph, One> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
374}
375
376#[cfg(feature = "example-db")]
377pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
378    vec![crate::example_db::specs::ModelExampleSpec {
379        id: "steiner_tree_in_graphs_simplegraph",
380        instance: Box::new(SteinerTreeInGraphs::new(
381            SimpleGraph::new(
382                6,
383                vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5)],
384            ),
385            vec![0, 3, 5],
386            vec![3, 2, 4, 1, 2, 3, 1],
387        )),
388        // Optimal: edges {0,2}(w=2), {2,3}(w=1), {2,5}(w=2) = weight 5
389        optimal_config: serde_json::json!(vec![false, true, false, true, true, false, false]),
390        optimal_value: serde_json::json!(5),
391    }]
392}
393
394#[cfg(test)]
395#[path = "../../unit_tests/models/graph/steiner_tree_in_graphs.rs"]
396mod tests;