Skip to main content

problemreductions/models/graph/
bounded_diameter_spanning_tree.rs

1//! Bounded Diameter Spanning Tree problem implementation.
2//!
3//! Given a graph G = (V, E) with edge weights, a weight bound B, and a diameter
4//! bound D, determine whether G has a spanning tree with total weight at most B
5//! and diameter (longest shortest path in edges) at most D.
6
7use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
8use crate::topology::{Graph, SimpleGraph};
9use crate::traits::Problem;
10use crate::types::WeightElement;
11use crate::variant::VariantParam;
12use num_traits::Zero;
13use serde::{Deserialize, Serialize};
14use std::collections::VecDeque;
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "BoundedDiameterSpanningTree",
19        display_name: "Bounded Diameter Spanning Tree",
20        aliases: &[],
21        dimensions: &[
22            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
23            VariantDimension::new("weight", "i64", &["i64"]),
24        ],
25        category: crate::registry::ProblemCategory::Graph,
26        module_path: module_path!(),
27        description: "Does G have a spanning tree with total weight <= B and diameter <= D?",
28        fields: BoundedDiameterSpanningTreeCreateSpec::FIELDS,
29    }
30}
31
32/// Bounded Diameter Spanning Tree problem.
33///
34/// Given an undirected graph G = (V, E) with positive edge weights w(e), a
35/// weight bound B, and a diameter bound D, determine whether G contains a
36/// spanning tree T such that the total weight of T is at most B and the
37/// diameter of T (the longest shortest path measured in number of edges) is
38/// at most D.
39///
40/// Each configuration entry corresponds to an edge (in the order returned by
41/// `graph.edges()`), with value 0 (not selected) or 1 (selected).
42///
43/// # Type Parameters
44///
45/// * `G` - Graph type (e.g., SimpleGraph)
46/// * `W` - Edge weight type (e.g., i64)
47///
48/// # Example
49///
50/// ```
51/// use problemreductions::models::graph::BoundedDiameterSpanningTree;
52/// use problemreductions::topology::SimpleGraph;
53/// use problemreductions::{Problem, BruteForce};
54///
55/// let graph = SimpleGraph::new(5, vec![(0,1),(0,2),(0,3),(1,2),(1,4),(2,3),(3,4)]);
56/// let problem = BoundedDiameterSpanningTree::new(graph, vec![1,2,1,1,2,1,1], 5, 3);
57///
58/// let solver = BruteForce::new();
59/// let solution = solver.solve(&problem).unwrap();
60/// assert!(solution.is_some());
61/// ```
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(bound(
64    deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::Deserialize<'de>"
65))]
66pub struct BoundedDiameterSpanningTree<G, W: WeightElement> {
67    /// The underlying graph.
68    graph: G,
69    /// Weight for each edge in graph-edge order.
70    edge_weights: Vec<W>,
71    /// Upper bound B on total tree weight.
72    weight_bound: W::Sum,
73    /// Upper bound D on tree diameter (in edges).
74    diameter_bound: usize,
75    /// Ordered edge list (mirrors `graph.edges()` order).
76    edge_list: Vec<(usize, usize)>,
77}
78
79#[derive(Debug, Deserialize, crate::CreateSpec)]
80struct BoundedDiameterSpanningTreeCreateSpec {
81    #[create(codec = "edge-list")]
82    graph: Vec<(usize, usize)>,
83    num_vertices: Option<usize>,
84    #[create(codec = "comma-separated")]
85    edge_weights: Option<Vec<i64>>,
86    weight_bound: i64,
87    diameter_bound: usize,
88}
89
90impl TryFrom<BoundedDiameterSpanningTreeCreateSpec>
91    for BoundedDiameterSpanningTree<SimpleGraph, i64>
92{
93    type Error = crate::registry::ConstructionError;
94
95    fn try_from(spec: BoundedDiameterSpanningTreeCreateSpec) -> Result<Self, Self::Error> {
96        let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
97        let edge_weights = spec
98            .edge_weights
99            .unwrap_or_else(|| vec![1; graph.num_edges()]);
100        if edge_weights.len() != graph.num_edges() {
101            return Err(format!(
102                "edge_weights has length {}, expected {}",
103                edge_weights.len(),
104                graph.num_edges()
105            )
106            .into());
107        }
108        if edge_weights.iter().any(|&weight| weight <= 0) {
109            return Err("edge_weights must be positive".to_string().into());
110        }
111        if spec.weight_bound <= 0 {
112            return Err("weight_bound must be positive".to_string().into());
113        }
114        if spec.diameter_bound == 0 {
115            return Err("diameter_bound must be at least 1".to_string().into());
116        }
117        Ok(Self::new(
118            graph,
119            edge_weights,
120            spec.weight_bound,
121            spec.diameter_bound,
122        ))
123    }
124}
125
126fn simple_graph_from_create(
127    edges: Vec<(usize, usize)>,
128    num_vertices: Option<usize>,
129) -> Result<SimpleGraph, crate::registry::ConstructionError> {
130    if edges.is_empty() && num_vertices.is_none() {
131        return Err("num_vertices is required for an empty graph"
132            .to_string()
133            .into());
134    }
135    for (index, &(u, v)) in edges.iter().enumerate() {
136        if u == v {
137            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
138        }
139    }
140    let inferred = edges
141        .iter()
142        .flat_map(|&(u, v)| [u, v])
143        .max()
144        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
145        .transpose()?
146        .unwrap_or(0);
147    let num_vertices = num_vertices.unwrap_or(inferred);
148    if num_vertices < inferred {
149        return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into());
150    }
151    Ok(SimpleGraph::new(num_vertices, edges))
152}
153
154impl<G: Graph, W: WeightElement> BoundedDiameterSpanningTree<G, W> {
155    /// Create a new Bounded Diameter Spanning Tree instance.
156    ///
157    /// # Panics
158    /// Panics if `edge_weights` length does not match the graph's edge count,
159    /// if any edge weight is not positive, or if `diameter_bound` is zero.
160    pub fn new(
161        graph: G,
162        edge_weights: Vec<W>,
163        weight_bound: W::Sum,
164        diameter_bound: usize,
165    ) -> Self {
166        assert_eq!(
167            edge_weights.len(),
168            graph.num_edges(),
169            "edge_weights length must match num_edges"
170        );
171        let zero = W::Sum::zero();
172        assert!(
173            edge_weights.iter().all(|w| w.to_sum() > zero.clone()),
174            "All edge weights must be positive (> 0)"
175        );
176        assert!(weight_bound > zero, "weight_bound must be positive (> 0)");
177        assert!(diameter_bound >= 1, "diameter_bound must be at least 1");
178        let edge_list = graph.edges();
179        Self {
180            graph,
181            edge_weights,
182            weight_bound,
183            diameter_bound,
184            edge_list,
185        }
186    }
187
188    /// Get a reference to the underlying graph.
189    pub fn graph(&self) -> &G {
190        &self.graph
191    }
192
193    /// Get the edge weights.
194    pub fn edge_weights(&self) -> &[W] {
195        &self.edge_weights
196    }
197
198    /// Set new edge weights.
199    pub fn set_weights(&mut self, edge_weights: Vec<W>) {
200        assert_eq!(
201            edge_weights.len(),
202            self.graph.num_edges(),
203            "edge_weights length must match num_edges"
204        );
205        let zero = W::Sum::zero();
206        assert!(
207            edge_weights.iter().all(|w| w.to_sum() > zero.clone()),
208            "All edge weights must be positive (> 0)"
209        );
210        self.edge_weights = edge_weights;
211    }
212
213    /// Get the weight bound B.
214    pub fn weight_bound(&self) -> &W::Sum {
215        &self.weight_bound
216    }
217
218    /// Get the diameter bound D.
219    pub fn diameter_bound(&self) -> usize {
220        self.diameter_bound
221    }
222
223    /// Get the number of vertices in the underlying graph.
224    pub fn num_vertices(&self) -> usize {
225        self.graph.num_vertices()
226    }
227
228    /// Get the number of edges in the underlying graph.
229    pub fn num_edges(&self) -> usize {
230        self.graph.num_edges()
231    }
232
233    /// Get the ordered edge list.
234    pub fn edge_list(&self) -> &[(usize, usize)] {
235        &self.edge_list
236    }
237
238    /// Check whether this problem uses a non-unit weight type.
239    pub fn is_weighted(&self) -> bool {
240        !W::IS_UNIT
241    }
242
243    /// Compute the diameter of a tree given its adjacency list.
244    /// The diameter is the length (in number of edges) of the longest shortest
245    /// path between any two vertices in the tree.
246    fn tree_diameter(adj: &[Vec<usize>], n: usize) -> usize {
247        let mut max_dist = 0;
248        for start in 0..n {
249            if adj[start].is_empty() {
250                continue;
251            }
252            let mut dist = vec![usize::MAX; n];
253            dist[start] = 0;
254            let mut queue = VecDeque::new();
255            queue.push_back(start);
256            while let Some(v) = queue.pop_front() {
257                for &u in &adj[v] {
258                    if dist[u] == usize::MAX {
259                        dist[u] = dist[v] + 1;
260                        if dist[u] > max_dist {
261                            max_dist = dist[u];
262                        }
263                        queue.push_back(u);
264                    }
265                }
266            }
267        }
268        max_dist
269    }
270}
271
272impl<G, W> Problem for BoundedDiameterSpanningTree<G, W>
273where
274    G: Graph + VariantParam,
275    W: WeightElement + VariantParam,
276{
277    const NAME: &'static str = "BoundedDiameterSpanningTree";
278    type Solution = Vec<bool>;
279    type Value = crate::types::Or;
280
281    crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
282
283    fn variant() -> Vec<(&'static str, &'static str)> {
284        crate::variant_params![G, W]
285    }
286
287    fn evaluate(
288        &self,
289        config: &Self::Solution,
290    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
291        Ok({
292            crate::types::Or({
293                let n = self.graph.num_vertices();
294                if config.len() != self.edge_list.len() {
295                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
296                        "edge-selection length does not match the graph".into(),
297                    ));
298                }
299
300                // Collect selected edges
301                let selected_indices: Vec<usize> = config
302                    .iter()
303                    .enumerate()
304                    .filter(|(_, &v)| v)
305                    .map(|(i, _)| i)
306                    .collect();
307
308                // A spanning tree on n vertices must have exactly n-1 edges
309                if n == 0 {
310                    return Ok(crate::types::Or(selected_indices.is_empty()));
311                }
312                if selected_indices.len() != n - 1 {
313                    return Ok(crate::types::Or(false));
314                }
315
316                // Build adjacency list and compute total weight
317                let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
318                let mut total_weight = W::Sum::zero();
319                for &idx in &selected_indices {
320                    let (u, v) = self.edge_list[idx];
321                    adj[u].push(v);
322                    adj[v].push(u);
323                    total_weight = W::checked_add_to_sum(
324                        total_weight,
325                        self.edge_weights[idx].to_sum(),
326                        "summing bounded-diameter spanning tree weights",
327                    )?;
328                }
329
330                // Check weight bound
331                if total_weight > self.weight_bound.clone() {
332                    return Ok(crate::types::Or(false));
333                }
334
335                // Check connectivity using BFS
336                let mut visited = vec![false; n];
337                let mut queue = VecDeque::new();
338                visited[0] = true;
339                queue.push_back(0);
340                let mut count = 1;
341                while let Some(v) = queue.pop_front() {
342                    for &u in &adj[v] {
343                        if !visited[u] {
344                            visited[u] = true;
345                            count += 1;
346                            queue.push_back(u);
347                        }
348                    }
349                }
350
351                if count != n {
352                    return Ok(crate::types::Or(false));
353                }
354
355                // Check diameter bound (BFS from each vertex)
356                let diameter = Self::tree_diameter(&adj, n);
357                diameter <= self.diameter_bound
358            })
359        })
360    }
361}
362
363impl<G, W> crate::solvers::BruteForceProblem for BoundedDiameterSpanningTree<G, W>
364where
365    G: Graph + VariantParam,
366    W: WeightElement + VariantParam,
367{
368    fn dimensions(&self) -> Vec<usize> {
369        vec![2; self.edge_list.len()]
370    }
371}
372
373crate::declare_variants! {
374    default BoundedDiameterSpanningTree<SimpleGraph, i64> => "num_vertices ^ num_vertices" create BoundedDiameterSpanningTreeCreateSpec,
375}
376
377crate::register_brute_force! {
378    BoundedDiameterSpanningTree<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
379}
380
381#[cfg(feature = "example-db")]
382pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
383    // 5 vertices, 7 edges with weights: (0,1,1),(0,2,2),(0,3,1),(1,2,1),(1,4,2),(2,3,1),(3,4,1)
384    // B=5, D=3
385    // Tree: edges (0,1),(0,3),(2,3),(3,4) → edge indices 0,2,5,6
386    // Config: [1,0,1,0,0,1,1] → weight = 1+1+1+1 = 4 ≤ 5, diameter = 3 ≤ 3
387    vec![crate::example_db::specs::ModelExampleSpec {
388        id: "bounded_diameter_spanning_tree_simplegraph",
389        instance: Box::new(BoundedDiameterSpanningTree::new(
390            SimpleGraph::new(
391                5,
392                vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 4), (2, 3), (3, 4)],
393            ),
394            vec![1, 2, 1, 1, 2, 1, 1],
395            5,
396            3,
397        )),
398        optimal_config: serde_json::json!(vec![true, false, true, false, false, true, true]),
399        optimal_value: serde_json::json!(true),
400    }]
401}
402
403#[cfg(test)]
404#[path = "../../unit_tests/models/graph/bounded_diameter_spanning_tree.rs"]
405mod tests;