Skip to main content

problemreductions/models/misc/
stacker_crane.rs

1//! Stacker Crane problem implementation.
2//!
3//! Given required directed arcs and optional undirected edges, find a closed
4//! walk that traverses every required arc in some order and minimizes the
5//! total route length.
6
7use crate::registry::{CreateSpec, ProblemSchemaEntry};
8use crate::traits::Problem;
9use crate::types::Min;
10use serde::{Deserialize, Serialize};
11use std::cmp::Reverse;
12use std::collections::BinaryHeap;
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "StackerCrane",
17        display_name: "Stacker Crane",
18        aliases: &[],
19        dimensions: &[],
20        category: crate::registry::ProblemCategory::Misc,
21        module_path: module_path!(),
22        description: "Find a closed walk that traverses each required directed arc and minimizes total length",
23        fields: StackerCraneCreateSpec::FIELDS,
24    }
25}
26
27/// The Stacker Crane problem.
28///
29/// A configuration is a permutation of the required arc indices. The walk
30/// traverses those arcs in the chosen order, connecting the head of each arc
31/// to the tail of the next arc by a shortest path in the mixed graph induced
32/// by the required directed arcs together with the undirected edges.
33/// The objective is to minimize the total walk length.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(try_from = "StackerCraneDef")]
36pub struct StackerCrane {
37    num_vertices: usize,
38    arcs: Vec<(usize, usize)>,
39    edges: Vec<(usize, usize)>,
40    arc_lengths: Vec<i64>,
41    edge_lengths: Vec<i64>,
42}
43
44#[derive(Debug, Deserialize, crate::CreateSpec)]
45struct StackerCraneCreateSpec {
46    /// Required directed arcs.
47    #[create(codec = "arc-list")]
48    arcs: Vec<(usize, usize)>,
49    /// Undirected connector edges.
50    #[create(name = "graph", codec = "edge-list")]
51    edges: Vec<(usize, usize)>,
52    /// Vertex count, needed to preserve isolated vertices.
53    num_vertices: Option<usize>,
54    /// Required-arc lengths; defaults to one per arc.
55    #[create(codec = "comma-separated")]
56    arc_lengths: Option<Vec<i64>>,
57    /// Connector-edge lengths; defaults to one per edge.
58    #[create(codec = "comma-separated")]
59    edge_lengths: Option<Vec<i64>>,
60}
61
62impl TryFrom<StackerCraneCreateSpec> for StackerCrane {
63    type Error = crate::registry::ConstructionError;
64
65    fn try_from(spec: StackerCraneCreateSpec) -> Result<Self, Self::Error> {
66        if spec.arcs.is_empty() {
67            return Err("arcs must be non-empty".to_string().into());
68        }
69        if spec.edges.is_empty() && spec.num_vertices.is_none() {
70            return Err("num_vertices is required for an empty graph"
71                .to_string()
72                .into());
73        }
74        for (index, &(u, v)) in spec.edges.iter().enumerate() {
75            if u == v {
76                return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
77            }
78        }
79        let inferred_arcs = inferred_vertex_count(&spec.arcs)?;
80        let inferred_edges = inferred_vertex_count(&spec.edges)?;
81        let num_vertices = match spec.num_vertices {
82            Some(count) => count,
83            None if inferred_arcs == inferred_edges => inferred_arcs,
84            None => {
85                return Err(format!(
86                    "directed and undirected inputs infer different vertex counts ({inferred_arcs} and {inferred_edges}); provide num_vertices"
87                ).into())
88            }
89        };
90        if num_vertices < inferred_arcs || num_vertices < inferred_edges {
91            return Err(format!(
92                "num_vertices {num_vertices} is too small for the provided endpoints"
93            )
94            .into());
95        }
96        let arc_lengths = spec.arc_lengths.unwrap_or_else(|| vec![1; spec.arcs.len()]);
97        let edge_lengths = spec
98            .edge_lengths
99            .unwrap_or_else(|| vec![1; spec.edges.len()]);
100        Self::try_new(
101            num_vertices,
102            spec.arcs,
103            spec.edges,
104            arc_lengths,
105            edge_lengths,
106        )
107    }
108}
109
110fn inferred_vertex_count(
111    pairs: &[(usize, usize)],
112) -> Result<usize, crate::registry::ConstructionError> {
113    Ok(pairs
114        .iter()
115        .flat_map(|&(u, v)| [u, v])
116        .max()
117        .map(|vertex| {
118            vertex
119                .checked_add(1)
120                .ok_or("vertex count overflows usize".to_string())
121        })
122        .transpose()
123        .map(|count| count.unwrap_or(0))?)
124}
125
126impl StackerCrane {
127    /// Create a new Stacker Crane instance.
128    ///
129    /// # Panics
130    ///
131    /// Panics if the instance data are inconsistent or contain negative
132    /// lengths.
133    pub fn new(
134        num_vertices: usize,
135        arcs: Vec<(usize, usize)>,
136        edges: Vec<(usize, usize)>,
137        arc_lengths: Vec<i64>,
138        edge_lengths: Vec<i64>,
139    ) -> Self {
140        Self::try_new(num_vertices, arcs, edges, arc_lengths, edge_lengths)
141            .unwrap_or_else(|message| panic!("{message}"))
142    }
143
144    /// Create a new Stacker Crane instance, returning validation errors.
145    pub fn try_new(
146        num_vertices: usize,
147        arcs: Vec<(usize, usize)>,
148        edges: Vec<(usize, usize)>,
149        arc_lengths: Vec<i64>,
150        edge_lengths: Vec<i64>,
151    ) -> Result<Self, crate::registry::ConstructionError> {
152        if arc_lengths.len() != arcs.len() {
153            return Err("arc_lengths length must match arcs length"
154                .to_string()
155                .into());
156        }
157        if edge_lengths.len() != edges.len() {
158            return Err("edge_lengths length must match edges length"
159                .to_string()
160                .into());
161        }
162        for (arc_index, &(tail, head)) in arcs.iter().enumerate() {
163            if tail >= num_vertices || head >= num_vertices {
164                return Err(format!(
165                    "arc {arc_index} endpoint out of range for {num_vertices} vertices"
166                )
167                .into());
168            }
169        }
170        for (edge_index, &(u, v)) in edges.iter().enumerate() {
171            if u >= num_vertices || v >= num_vertices {
172                return Err(format!(
173                    "edge {edge_index} endpoint out of range for {num_vertices} vertices"
174                )
175                .into());
176            }
177        }
178        for (arc_index, &length) in arc_lengths.iter().enumerate() {
179            if length < 0 {
180                return Err(format!("arc length {arc_index} must be nonnegative").into());
181            }
182        }
183        for (edge_index, &length) in edge_lengths.iter().enumerate() {
184            if length < 0 {
185                return Err(format!("edge length {edge_index} must be nonnegative").into());
186            }
187        }
188
189        Ok(Self {
190            num_vertices,
191            arcs,
192            edges,
193            arc_lengths,
194            edge_lengths,
195        })
196    }
197
198    /// Get the number of vertices in the mixed graph.
199    pub fn num_vertices(&self) -> usize {
200        self.num_vertices
201    }
202
203    /// Get the required directed arcs.
204    pub fn arcs(&self) -> &[(usize, usize)] {
205        &self.arcs
206    }
207
208    /// Get the available undirected edges.
209    pub fn edges(&self) -> &[(usize, usize)] {
210        &self.edges
211    }
212
213    /// Get the required arc lengths.
214    pub fn arc_lengths(&self) -> &[i64] {
215        &self.arc_lengths
216    }
217
218    /// Get the undirected edge lengths.
219    pub fn edge_lengths(&self) -> &[i64] {
220        &self.edge_lengths
221    }
222
223    /// Get the number of required arcs.
224    pub fn num_arcs(&self) -> usize {
225        self.arcs.len()
226    }
227
228    /// Get the number of undirected edges.
229    pub fn num_edges(&self) -> usize {
230        self.edges.len()
231    }
232
233    fn is_arc_permutation(&self, config: &[usize]) -> bool {
234        if config.len() != self.num_arcs() {
235            return false;
236        }
237
238        let mut seen = vec![false; self.num_arcs()];
239        for &arc_index in config {
240            if arc_index >= self.num_arcs() || seen[arc_index] {
241                return false;
242            }
243            seen[arc_index] = true;
244        }
245
246        true
247    }
248
249    fn mixed_graph_adjacency(&self) -> Vec<Vec<(usize, i64)>> {
250        let mut adjacency = vec![Vec::new(); self.num_vertices];
251
252        for (&(tail, head), &length) in self.arcs.iter().zip(&self.arc_lengths) {
253            adjacency[tail].push((head, length));
254        }
255
256        for (&(u, v), &length) in self.edges.iter().zip(&self.edge_lengths) {
257            adjacency[u].push((v, length));
258            adjacency[v].push((u, length));
259        }
260
261        adjacency
262    }
263
264    fn shortest_path_length(
265        &self,
266        adjacency: &[Vec<(usize, i64)>],
267        source: usize,
268        target: usize,
269    ) -> Option<i64> {
270        if source == target {
271            return Some(0);
272        }
273
274        let mut dist = vec![i64::MAX; self.num_vertices];
275        let mut heap = BinaryHeap::new();
276        dist[source] = 0;
277        heap.push((Reverse(0i64), source));
278
279        while let Some((Reverse(cost), node)) = heap.pop() {
280            if cost > dist[node] {
281                continue;
282            }
283            if node == target {
284                return Some(cost);
285            }
286
287            for &(next, length) in &adjacency[node] {
288                let next_cost = cost.checked_add(length)?;
289                if next_cost < dist[next] {
290                    dist[next] = next_cost;
291                    heap.push((Reverse(next_cost), next));
292                }
293            }
294        }
295
296        None
297    }
298
299    /// Compute the total closed-walk length induced by a configuration.
300    ///
301    /// Returns `None` for invalid permutations, unreachable connector paths,
302    /// or arithmetic overflow.
303    pub fn closed_walk_length(&self, config: &[usize]) -> Option<i64> {
304        if !self.is_arc_permutation(config) {
305            return None;
306        }
307        if config.is_empty() {
308            return Some(0);
309        }
310
311        let adjacency = self.mixed_graph_adjacency();
312        let mut total = 0i64;
313
314        for position in 0..config.len() {
315            let arc_index = config[position];
316            let next_arc_index = config[(position + 1) % config.len()];
317            let (_, arc_head) = self.arcs[arc_index];
318            let (next_arc_tail, _) = self.arcs[next_arc_index];
319
320            total = total.checked_add(self.arc_lengths[arc_index])?;
321            total = total.checked_add(self.shortest_path_length(
322                &adjacency,
323                arc_head,
324                next_arc_tail,
325            )?)?;
326        }
327
328        Some(total)
329    }
330}
331
332impl Problem for StackerCrane {
333    const NAME: &'static str = "StackerCrane";
334    type Solution = Vec<usize>;
335    type Value = Min<i64>;
336
337    crate::problem_parameters![
338        ("num_arcs", num_arcs),
339        ("num_edges", num_edges),
340        ("num_vertices", num_vertices),
341    ];
342
343    fn variant() -> Vec<(&'static str, &'static str)> {
344        crate::variant_params![]
345    }
346
347    fn evaluate(
348        &self,
349        config: &Self::Solution,
350    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
351        if config.len() != self.num_arcs() {
352            return Err(crate::traits::EvaluationError::InvalidConfiguration(
353                "arc ordering length does not match the required arcs".into(),
354            ));
355        }
356        if config.iter().any(|&arc| arc >= self.num_arcs()) {
357            return Err(crate::traits::EvaluationError::InvalidConfiguration(
358                "arc ordering contains an out-of-range arc".into(),
359            ));
360        }
361        Ok({
362            match self.closed_walk_length(config) {
363                Some(total) => Min(Some(total)),
364                None => Min(None),
365            }
366        })
367    }
368}
369
370impl crate::solvers::BruteForceProblem for StackerCrane {
371    fn dimensions(&self) -> Vec<usize> {
372        vec![self.num_arcs(); self.num_arcs()]
373    }
374}
375
376crate::declare_variants! {
377    default StackerCrane => "num_vertices^2 * 2^num_arcs" create StackerCraneCreateSpec,
378}
379
380crate::register_brute_force! {
381    StackerCrane,
382}
383
384#[derive(Debug, Clone, Deserialize)]
385struct StackerCraneDef {
386    num_vertices: usize,
387    arcs: Vec<(usize, usize)>,
388    edges: Vec<(usize, usize)>,
389    arc_lengths: Vec<i64>,
390    edge_lengths: Vec<i64>,
391}
392
393impl TryFrom<StackerCraneDef> for StackerCrane {
394    type Error = crate::registry::ConstructionError;
395
396    fn try_from(value: StackerCraneDef) -> Result<Self, Self::Error> {
397        Self::try_new(
398            value.num_vertices,
399            value.arcs,
400            value.edges,
401            value.arc_lengths,
402            value.edge_lengths,
403        )
404    }
405}
406
407#[cfg(feature = "example-db")]
408pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
409    vec![crate::example_db::specs::ModelExampleSpec {
410        id: "stacker_crane",
411        instance: Box::new(StackerCrane::new(
412            6,
413            vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)],
414            vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)],
415            vec![3, 4, 2, 5, 3],
416            vec![2, 1, 3, 2, 1, 4, 3],
417        )),
418        optimal_config: serde_json::json!(vec![0, 2, 1, 4, 3]),
419        optimal_value: serde_json::json!(20),
420    }]
421}
422
423#[cfg(test)]
424#[path = "../../unit_tests/models/misc/stacker_crane.rs"]
425mod tests;