Skip to main content

problemreductions/models/graph/
kth_best_spanning_tree.rs

1//! Kth Best Spanning Tree problem implementation.
2//!
3//! Given a weighted graph, determine whether it contains `k` distinct spanning
4//! trees whose total weights are all at most a prescribed bound.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::WeightElement;
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "KthBestSpanningTree",
17        display_name: "Kth Best Spanning Tree",
18        aliases: &[],
19        dimensions: &[VariantDimension::new("weight", "i64", &["i64"])],
20        category: crate::registry::ProblemCategory::Graph,
21        module_path: module_path!(),
22        description: "Do there exist k distinct spanning trees with total weight at most B?",
23        fields: KthBestSpanningTreeCreateSpec::FIELDS,
24    }
25}
26
27/// Kth Best Spanning Tree.
28///
29/// Given an undirected graph `G = (V, E)`, non-negative edge weights `w(e)`,
30/// a positive integer `k`, and a bound `B`, determine whether there are `k`
31/// distinct spanning trees of `G` whose total weights are all at most `B`.
32///
33/// # Representation
34///
35/// A configuration is `k` consecutive binary blocks of length `|E|`.
36/// Each block selects the edges of one candidate spanning tree.
37#[derive(Debug, Clone, Serialize)]
38pub struct KthBestSpanningTree<W: WeightElement> {
39    graph: SimpleGraph,
40    weights: Vec<W>,
41    k: usize,
42    bound: W::Sum,
43}
44
45#[derive(Deserialize)]
46#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))]
47struct KthBestSpanningTreeData<W: WeightElement> {
48    graph: SimpleGraph,
49    weights: Vec<W>,
50    k: usize,
51    bound: W::Sum,
52}
53
54impl<'de, W> Deserialize<'de> for KthBestSpanningTree<W>
55where
56    W: WeightElement + Deserialize<'de>,
57    W::Sum: Deserialize<'de>,
58{
59    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
60        let data = KthBestSpanningTreeData::<W>::deserialize(deserializer)?;
61        Self::try_new(data.graph, data.weights, data.k, data.bound)
62            .map_err(serde::de::Error::custom)
63    }
64}
65
66#[derive(Debug, Deserialize, crate::CreateSpec)]
67struct KthBestSpanningTreeCreateSpec {
68    #[create(codec = "edge-list")]
69    graph: Vec<(usize, usize)>,
70    num_vertices: Option<usize>,
71    #[create(codec = "comma-separated")]
72    edge_weights: Option<Vec<i64>>,
73    k: usize,
74    bound: i64,
75}
76
77impl TryFrom<KthBestSpanningTreeCreateSpec> for KthBestSpanningTree<i64> {
78    type Error = crate::registry::ConstructionError;
79
80    fn try_from(spec: KthBestSpanningTreeCreateSpec) -> Result<Self, Self::Error> {
81        let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
82        let weights = spec
83            .edge_weights
84            .unwrap_or_else(|| vec![1; graph.num_edges()]);
85        Self::try_new(graph, weights, spec.k, spec.bound)
86    }
87}
88
89fn simple_graph_from_create(
90    edges: Vec<(usize, usize)>,
91    num_vertices: Option<usize>,
92) -> Result<SimpleGraph, crate::registry::ConstructionError> {
93    if edges.is_empty() && num_vertices.is_none() {
94        return Err("num_vertices is required for an empty graph"
95            .to_string()
96            .into());
97    }
98    for (index, &(u, v)) in edges.iter().enumerate() {
99        if u == v {
100            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
101        }
102    }
103    let inferred = edges
104        .iter()
105        .flat_map(|&(u, v)| [u, v])
106        .max()
107        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
108        .transpose()?
109        .unwrap_or(0);
110    let num_vertices = num_vertices.unwrap_or(inferred);
111    if num_vertices < inferred {
112        return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into());
113    }
114    Ok(SimpleGraph::new(num_vertices, edges))
115}
116
117impl<W: WeightElement> KthBestSpanningTree<W> {
118    /// Create a new KthBestSpanningTree instance.
119    ///
120    /// # Panics
121    ///
122    /// Panics if the number of weights does not match the number of edges, or
123    /// if `k` is zero.
124    pub fn new(graph: SimpleGraph, weights: Vec<W>, k: usize, bound: W::Sum) -> Self {
125        Self::try_new(graph, weights, k, bound).unwrap_or_else(|error| panic!("{error}"))
126    }
127
128    fn try_new(
129        graph: SimpleGraph,
130        weights: Vec<W>,
131        k: usize,
132        bound: W::Sum,
133    ) -> Result<Self, crate::registry::ConstructionError> {
134        if weights.len() != graph.num_edges() {
135            return Err(crate::registry::ConstructionError::length_mismatch(
136                "weights",
137                weights.len(),
138                graph.num_edges(),
139            ));
140        }
141        if k == 0 {
142            return Err("k must be positive".into());
143        }
144
145        Ok(Self {
146            graph,
147            weights,
148            k,
149            bound,
150        })
151    }
152
153    /// Get the underlying graph.
154    pub fn graph(&self) -> &SimpleGraph {
155        &self.graph
156    }
157
158    /// Get the edge weights.
159    pub fn weights(&self) -> &[W] {
160        &self.weights
161    }
162
163    /// Get the requested number of trees.
164    pub fn k(&self) -> usize {
165        self.k
166    }
167
168    /// Get the weight bound.
169    pub fn bound(&self) -> &W::Sum {
170        &self.bound
171    }
172
173    /// Get the number of vertices.
174    pub fn num_vertices(&self) -> usize {
175        self.graph.num_vertices()
176    }
177
178    /// Get the number of edges.
179    pub fn num_edges(&self) -> usize {
180        self.graph.num_edges()
181    }
182
183    /// Check whether the problem uses a non-unit weight type.
184    pub fn is_weighted(&self) -> bool {
185        !W::IS_UNIT
186    }
187
188    /// Check whether a configuration satisfies the problem.
189    pub fn is_valid_solution(
190        &self,
191        config: &[Vec<bool>],
192    ) -> Result<bool, crate::traits::EvaluationError> {
193        if config.len() != self.k
194            || config
195                .iter()
196                .any(|tree| tree.len() != self.graph.num_edges())
197        {
198            return Ok(false);
199        }
200
201        let edges = self.graph.edges();
202        if !self.blocks_are_pairwise_distinct(config) {
203            return Ok(false);
204        }
205        for tree in config {
206            if !self.block_is_valid_tree(tree, &edges)? {
207                return Ok(false);
208            }
209        }
210        Ok(true)
211    }
212
213    fn block_is_valid_tree(
214        &self,
215        block: &[bool],
216        edges: &[(usize, usize)],
217    ) -> Result<bool, crate::traits::EvaluationError> {
218        if block.len() != edges.len() {
219            return Ok(false);
220        }
221
222        let num_vertices = self.graph.num_vertices();
223        let selected_count = block.iter().filter(|&&selected| selected).count();
224        if selected_count != num_vertices.saturating_sub(1) {
225            return Ok(false);
226        }
227
228        let mut total_weight = W::Sum::zero();
229        let mut adjacency = vec![Vec::new(); num_vertices];
230        let mut start = None;
231
232        for (idx, &selected) in block.iter().enumerate() {
233            if !selected {
234                continue;
235            }
236            total_weight = W::checked_add_to_sum(
237                total_weight,
238                self.weights[idx].to_sum(),
239                "summing spanning tree edge weights",
240            )?;
241            let (u, v) = edges[idx];
242            adjacency[u].push(v);
243            adjacency[v].push(u);
244            if start.is_none() {
245                start = Some(u);
246            }
247        }
248
249        if total_weight > self.bound {
250            return Ok(false);
251        }
252
253        if num_vertices <= 1 {
254            return Ok(true);
255        }
256
257        // SAFETY: num_vertices > 1 and selected_count == num_vertices - 1 > 0,
258        // so at least one edge was selected and `start` is Some.
259        let start = start.expect("at least one selected edge");
260
261        let mut visited = vec![false; num_vertices];
262        let mut queue = VecDeque::new();
263        visited[start] = true;
264        queue.push_back(start);
265
266        while let Some(vertex) = queue.pop_front() {
267            for &neighbor in &adjacency[vertex] {
268                if !visited[neighbor] {
269                    visited[neighbor] = true;
270                    queue.push_back(neighbor);
271                }
272            }
273        }
274
275        Ok(visited.into_iter().all(|seen| seen))
276    }
277
278    fn blocks_are_pairwise_distinct(&self, config: &[Vec<bool>]) -> bool {
279        for left in 0..config.len() {
280            for right in (left + 1)..config.len() {
281                if config[left] == config[right] {
282                    return false;
283                }
284            }
285        }
286        true
287    }
288}
289
290impl<W> Problem for KthBestSpanningTree<W>
291where
292    W: WeightElement + crate::variant::VariantParam,
293{
294    const NAME: &'static str = "KthBestSpanningTree";
295    type Solution = Vec<Vec<bool>>;
296    type Value = crate::types::Or;
297
298    crate::problem_parameters![
299        ("num_vertices", num_vertices),
300        ("num_edges", num_edges),
301        ("k", k),
302    ];
303
304    fn variant() -> Vec<(&'static str, &'static str)> {
305        crate::variant_params![W]
306    }
307
308    fn evaluate(
309        &self,
310        solution: &Self::Solution,
311    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
312        if solution.len() != self.k
313            || solution
314                .iter()
315                .any(|tree| tree.len() != self.graph.num_edges())
316        {
317            return Err(crate::traits::EvaluationError::InvalidConfiguration(
318                "spanning-tree collection dimensions do not match the instance".into(),
319            ));
320        }
321        Ok(crate::types::Or(self.is_valid_solution(solution)?))
322    }
323}
324
325impl<W> crate::solvers::BruteForceProblem for KthBestSpanningTree<W>
326where
327    W: WeightElement + crate::variant::VariantParam,
328{
329    fn dimensions(&self) -> Vec<usize> {
330        vec![2; self.k * self.graph.num_edges()]
331    }
332}
333
334#[cfg(feature = "example-db")]
335pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
336    // K4 with weights [1,1,2,2,2,3], k=2, B=4.
337    // 16 spanning trees; exactly 2 have weight ≤ 4 (both weight 4):
338    //   {01,02,03} (star at 0) and {01,02,13}.
339    // Satisfying configs = 2 (the two orderings of this pair).
340    // 12 variables → 2^12 = 4096 configs, fast to enumerate.
341    let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
342    let problem = KthBestSpanningTree::new(graph, vec![1, 1, 2, 2, 2, 3], 2, 4);
343    vec![crate::example_db::specs::ModelExampleSpec {
344        id: "kth_best_spanning_tree",
345        instance: Box::new(problem),
346        optimal_config: serde_json::json!([
347            [true, true, true, false, false, false],
348            [true, true, false, false, true, false]
349        ]),
350        optimal_value: serde_json::json!(true),
351    }]
352}
353
354crate::declare_variants! {
355    default KthBestSpanningTree<i64> => "2^(num_edges * k)" create KthBestSpanningTreeCreateSpec,
356}
357
358crate::register_brute_force! {
359    KthBestSpanningTree<i64> decode |problem: &KthBestSpanningTree<i64>, indices: Vec<usize>| indices.chunks(problem.num_edges()).map(crate::config::config_to_bits).collect(),
360}
361
362#[cfg(test)]
363#[path = "../../unit_tests/models/graph/kth_best_spanning_tree.rs"]
364mod tests;