Skip to main content

problemreductions/models/misc/
optimum_communication_spanning_tree.rs

1//! Optimum Communication Spanning Tree problem implementation.
2//!
3//! Given a complete graph K_n with edge weights w(e) and communication
4//! requirements r(u,v) for each vertex pair, find a spanning tree T that
5//! minimizes the total communication cost: sum_{u<v} r(u,v) * W_T(u,v),
6//! where W_T(u,v) is the sum of edge weights on the unique path from u to v in T.
7
8use crate::registry::{CreateSpec, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "OptimumCommunicationSpanningTree",
17        display_name: "Optimum Communication Spanning Tree",
18        aliases: &["OCST"],
19        dimensions: &[],
20        category: crate::registry::ProblemCategory::Misc,
21        module_path: module_path!(),
22        description: "Find spanning tree minimizing total weighted communication cost",
23        fields: OptimumCommunicationSpanningTreeCreateSpec::FIELDS,
24    }
25}
26
27/// The Optimum Communication Spanning Tree problem.
28///
29/// Given a complete graph K_n with edge weights w(e) >= 0 and communication
30/// requirements r(u,v) >= 0 for each vertex pair, find a spanning tree T
31/// minimizing the total communication cost:
32///
33///   sum_{u < v} r(u,v) * W_T(u,v)
34///
35/// where W_T(u,v) is the weight of the unique path between u and v in T.
36///
37/// # Representation
38///
39/// Each edge of K_n is assigned a binary variable (0 = not in tree, 1 = in tree).
40/// Edges are ordered lexicographically: (0,1), (0,2), ..., (0,n-1), (1,2), ..., (n-2,n-1).
41/// A valid spanning tree has exactly n-1 selected edges forming a connected subgraph.
42///
43/// # Example
44///
45/// ```
46/// use problemreductions::models::misc::OptimumCommunicationSpanningTree;
47/// use problemreductions::{Problem, BruteForce};
48///
49/// let problem = OptimumCommunicationSpanningTree::new(
50///     vec![
51///         vec![0, 1, 2],
52///         vec![1, 0, 3],
53///         vec![2, 3, 0],
54///     ],
55///     vec![
56///         vec![0, 1, 1],
57///         vec![1, 0, 1],
58///         vec![1, 1, 0],
59///     ],
60/// );
61/// let solver = BruteForce::new();
62/// let solution = solver.solve(&problem).unwrap();
63/// assert!(solution.is_some());
64/// ```
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(try_from = "OptimumCommunicationSpanningTreeData")]
67pub struct OptimumCommunicationSpanningTree {
68    num_vertices: usize,
69    edge_weights: Vec<Vec<i64>>,
70    requirements: Vec<Vec<i64>>,
71}
72
73#[derive(Deserialize)]
74struct OptimumCommunicationSpanningTreeData {
75    edge_weights: Vec<Vec<i64>>,
76    requirements: Vec<Vec<i64>>,
77}
78
79impl TryFrom<OptimumCommunicationSpanningTreeData> for OptimumCommunicationSpanningTree {
80    type Error = crate::registry::ConstructionError;
81    fn try_from(data: OptimumCommunicationSpanningTreeData) -> Result<Self, Self::Error> {
82        Self::try_new(data.edge_weights, data.requirements)
83    }
84}
85
86#[derive(Debug, Deserialize, crate::CreateSpec)]
87struct OptimumCommunicationSpanningTreeCreateSpec {
88    /// Number of vertices.
89    num_vertices: usize,
90    /// Symmetric weight matrix; defaults to unit off-diagonal weights.
91    edge_weights: Option<Vec<Vec<i64>>>,
92    /// Symmetric communication requirement matrix.
93    requirements: Vec<Vec<i64>>,
94}
95impl TryFrom<OptimumCommunicationSpanningTreeCreateSpec> for OptimumCommunicationSpanningTree {
96    type Error = crate::registry::ConstructionError;
97    fn try_from(spec: OptimumCommunicationSpanningTreeCreateSpec) -> Result<Self, Self::Error> {
98        let n = spec.num_vertices;
99        let edge_weights = spec.edge_weights.unwrap_or_else(|| {
100            (0..n)
101                .map(|i| (0..n).map(|j| i64::from(i != j)).collect())
102                .collect()
103        });
104        for (name, matrix) in [
105            ("edge_weights", &edge_weights),
106            ("requirements", &spec.requirements),
107        ] {
108            if matrix.len() != n {
109                return Err(format!("{name} must be a {n} x {n} matrix").into());
110            }
111        }
112        Self::try_new(edge_weights, spec.requirements)
113    }
114}
115
116impl OptimumCommunicationSpanningTree {
117    /// Create a new OptimumCommunicationSpanningTree instance.
118    ///
119    /// # Arguments
120    ///
121    /// * `edge_weights` - Symmetric n x n matrix with w(i,i) = 0 and w(i,j) >= 0.
122    /// * `requirements` - Symmetric n x n matrix with r(i,i) = 0 and r(i,j) >= 0.
123    ///
124    /// # Panics
125    ///
126    /// Panics if the matrices are not square, not the same size, have nonzero
127    /// diagonals, are not symmetric, or contain negative entries.
128    pub fn new(edge_weights: Vec<Vec<i64>>, requirements: Vec<Vec<i64>>) -> Self {
129        Self::try_new(edge_weights, requirements).unwrap_or_else(|error| panic!("{error}"))
130    }
131
132    fn try_new(
133        edge_weights: Vec<Vec<i64>>,
134        requirements: Vec<Vec<i64>>,
135    ) -> Result<Self, crate::registry::ConstructionError> {
136        let n = edge_weights.len();
137        if n < 2 {
138            return Err("must have at least 2 vertices".into());
139        }
140        if requirements.len() != n {
141            return Err("requirements matrix must have same size as edge_weights".into());
142        }
143
144        for (i, row) in edge_weights.iter().enumerate() {
145            if row.len() != n {
146                return Err(format!(
147                    "edge_weights must be square: row {i} has length {} but expected {n}",
148                    row.len()
149                )
150                .into());
151            }
152            if row[i] != 0 {
153                return Err(format!(
154                    "diagonal of edge_weights must be zero: edge_weights[{i}][{i}] = {}",
155                    row[i]
156                )
157                .into());
158            }
159        }
160
161        for (i, row) in requirements.iter().enumerate() {
162            if row.len() != n {
163                return Err(format!(
164                    "requirements must be square: row {i} has length {} but expected {n}",
165                    row.len()
166                )
167                .into());
168            }
169            if row[i] != 0 {
170                return Err(format!(
171                    "diagonal of requirements must be zero: requirements[{i}][{i}] = {}",
172                    row[i]
173                )
174                .into());
175            }
176        }
177
178        // Check symmetry and non-negativity
179        for i in 0..n {
180            for j in (i + 1)..n {
181                if edge_weights[i][j] != edge_weights[j][i] {
182                    return Err(format!(
183                        "edge_weights must be symmetric: w[{i}][{j}]={} != w[{j}][{i}]={}",
184                        edge_weights[i][j], edge_weights[j][i]
185                    )
186                    .into());
187                }
188                if edge_weights[i][j] < 0 {
189                    return Err(format!(
190                        "edge_weights must be non-negative: w[{i}][{j}]={}",
191                        edge_weights[i][j]
192                    )
193                    .into());
194                }
195                if requirements[i][j] != requirements[j][i] {
196                    return Err(format!(
197                        "requirements must be symmetric: r[{i}][{j}]={} != r[{j}][{i}]={}",
198                        requirements[i][j], requirements[j][i]
199                    )
200                    .into());
201                }
202                if requirements[i][j] < 0 {
203                    return Err(format!(
204                        "requirements must be non-negative: r[{i}][{j}]={}",
205                        requirements[i][j]
206                    )
207                    .into());
208                }
209            }
210        }
211
212        Ok(Self {
213            num_vertices: n,
214            edge_weights,
215            requirements,
216        })
217    }
218
219    /// Returns the number of vertices.
220    pub fn num_vertices(&self) -> usize {
221        self.num_vertices
222    }
223
224    /// Returns the number of edges in the complete graph K_n.
225    pub fn num_edges(&self) -> usize {
226        self.num_vertices * (self.num_vertices - 1) / 2
227    }
228
229    /// Returns the edge weight matrix.
230    pub fn edge_weights(&self) -> &Vec<Vec<i64>> {
231        &self.edge_weights
232    }
233
234    /// Returns the requirements matrix.
235    pub fn requirements(&self) -> &Vec<Vec<i64>> {
236        &self.requirements
237    }
238
239    /// Returns the list of edges in lexicographic order: (0,1), (0,2), ..., (n-2,n-1).
240    pub fn edges(&self) -> Vec<(usize, usize)> {
241        let n = self.num_vertices;
242        let mut edges = Vec::with_capacity(self.num_edges());
243        for i in 0..n {
244            for j in (i + 1)..n {
245                edges.push((i, j));
246            }
247        }
248        edges
249    }
250
251    /// Map a pair (i, j) with i < j to its edge index.
252    pub fn edge_index(i: usize, j: usize, n: usize) -> usize {
253        debug_assert!(i < j && j < n);
254        i * n - i * (i + 1) / 2 + (j - i - 1)
255    }
256}
257
258/// Check if a configuration forms a valid spanning tree of K_n.
259fn is_valid_spanning_tree(n: usize, edges: &[(usize, usize)], config: &[bool]) -> bool {
260    if config.len() != edges.len() {
261        return false;
262    }
263
264    // Count selected edges: must be exactly n-1
265    let selected_count = config.iter().filter(|&&selected| selected).count();
266    if selected_count != n - 1 {
267        return false;
268    }
269
270    // Build adjacency and check connectivity via BFS
271    let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
272    for (idx, &sel) in config.iter().enumerate() {
273        if sel {
274            let (u, v) = edges[idx];
275            adj[u].push(v);
276            adj[v].push(u);
277        }
278    }
279
280    let mut visited = vec![false; n];
281    let mut queue = VecDeque::new();
282    visited[0] = true;
283    queue.push_back(0);
284    while let Some(v) = queue.pop_front() {
285        for &u in &adj[v] {
286            if !visited[u] {
287                visited[u] = true;
288                queue.push_back(u);
289            }
290        }
291    }
292
293    visited.iter().all(|&v| v)
294}
295
296/// Compute the communication cost of a spanning tree.
297///
298/// For each pair (u, v) with u < v, compute W_T(u,v) via BFS in the tree,
299/// then accumulate r(u,v) * W_T(u,v).
300fn communication_cost(
301    n: usize,
302    edges: &[(usize, usize)],
303    config: &[bool],
304    edge_weights: &[Vec<i64>],
305    requirements: &[Vec<i64>],
306) -> Result<i64, crate::traits::EvaluationError> {
307    // Build weighted adjacency list for the tree
308    let mut adj: Vec<Vec<(usize, i64)>> = vec![vec![]; n];
309    for (idx, &sel) in config.iter().enumerate() {
310        if sel {
311            let (u, v) = edges[idx];
312            let w = edge_weights[u][v];
313            adj[u].push((v, w));
314            adj[v].push((u, w));
315        }
316    }
317
318    let mut total_cost: i64 = 0;
319
320    // For each source vertex, BFS to find path weights to all other vertices
321    for src in 0..n {
322        let mut dist = vec![-1i64; n];
323        dist[src] = 0;
324        let mut queue = VecDeque::new();
325        queue.push_back(src);
326        while let Some(u) = queue.pop_front() {
327            for &(v, w) in &adj[u] {
328                if dist[v] < 0 {
329                    dist[v] = dist[u].checked_add(w).ok_or_else(|| {
330                        crate::traits::EvaluationError::IntegerOverflow(
331                            "summing communication-tree path weights".to_string(),
332                        )
333                    })?;
334                    queue.push_back(v);
335                }
336            }
337        }
338
339        // Accumulate r(src, dst) * W_T(src, dst) for dst > src
340        for (dst, &d) in dist.iter().enumerate().skip(src + 1) {
341            let term = requirements[src][dst].checked_mul(d).ok_or_else(|| {
342                crate::traits::EvaluationError::IntegerOverflow(
343                    "multiplying communication requirement by path weight".to_string(),
344                )
345            })?;
346            total_cost = total_cost.checked_add(term).ok_or_else(|| {
347                crate::traits::EvaluationError::IntegerOverflow(
348                    "summing communication-tree costs".to_string(),
349                )
350            })?;
351        }
352    }
353
354    Ok(total_cost)
355}
356
357impl Problem for OptimumCommunicationSpanningTree {
358    const NAME: &'static str = "OptimumCommunicationSpanningTree";
359    type Solution = Vec<bool>;
360    type Value = Min<i64>;
361
362    crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
363
364    fn variant() -> Vec<(&'static str, &'static str)> {
365        crate::variant_params![]
366    }
367
368    fn evaluate(
369        &self,
370        config: &Self::Solution,
371    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
372        if config.len() != self.edges().len() {
373            return Err(crate::traits::EvaluationError::InvalidConfiguration(
374                "edge-selection length does not match the complete graph".into(),
375            ));
376        }
377        Ok({
378            let edges = self.edges();
379            if !is_valid_spanning_tree(self.num_vertices, &edges, config) {
380                return Ok(Min(None));
381            }
382            Min(Some(communication_cost(
383                self.num_vertices,
384                &edges,
385                config,
386                &self.edge_weights,
387                &self.requirements,
388            )?))
389        })
390    }
391}
392
393impl crate::solvers::BruteForceProblem for OptimumCommunicationSpanningTree {
394    fn dimensions(&self) -> Vec<usize> {
395        vec![2; self.num_edges()]
396    }
397}
398
399crate::declare_variants! {
400    default OptimumCommunicationSpanningTree => "2^num_edges" create OptimumCommunicationSpanningTreeCreateSpec,
401}
402
403crate::register_brute_force! {
404    OptimumCommunicationSpanningTree decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
405}
406
407#[cfg(feature = "example-db")]
408pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
409    // K4 example from issue #906
410    // Edge weights:
411    //   w(0,1)=1, w(0,2)=3, w(0,3)=2, w(1,2)=2, w(1,3)=4, w(2,3)=1
412    // Requirements:
413    //   r(0,1)=2, r(0,2)=1, r(0,3)=3, r(1,2)=1, r(1,3)=1, r(2,3)=2
414    // Optimal tree: {(0,1), (0,3), (2,3)} = edges at indices 0, 2, 5
415    // Optimal cost: 20
416    let edge_weights = vec![
417        vec![0, 1, 3, 2],
418        vec![1, 0, 2, 4],
419        vec![3, 2, 0, 1],
420        vec![2, 4, 1, 0],
421    ];
422    let requirements = vec![
423        vec![0, 2, 1, 3],
424        vec![2, 0, 1, 1],
425        vec![1, 1, 0, 2],
426        vec![3, 1, 2, 0],
427    ];
428    // Edges in lex order: (0,1)=idx0, (0,2)=idx1, (0,3)=idx2, (1,2)=idx3, (1,3)=idx4, (2,3)=idx5
429    // Optimal tree: {(0,1), (0,3), (2,3)} -> config = [1, 0, 1, 0, 0, 1]
430    vec![crate::example_db::specs::ModelExampleSpec {
431        id: "optimum_communication_spanning_tree",
432        instance: Box::new(OptimumCommunicationSpanningTree::new(
433            edge_weights,
434            requirements,
435        )),
436        optimal_config: serde_json::json!(vec![true, false, true, false, false, true]),
437        optimal_value: serde_json::json!(20),
438    }]
439}
440
441#[cfg(test)]
442#[path = "../../unit_tests/models/misc/optimum_communication_spanning_tree.rs"]
443mod tests;