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