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(crate::registry::ConstructionError::length_mismatch(
154                "arc_lengths",
155                arc_lengths.len(),
156                arcs.len(),
157            ));
158        }
159        if edge_lengths.len() != edges.len() {
160            return Err(crate::registry::ConstructionError::length_mismatch(
161                "edge_lengths",
162                edge_lengths.len(),
163                edges.len(),
164            ));
165        }
166        for (arc_index, &(tail, head)) in arcs.iter().enumerate() {
167            if tail >= num_vertices || head >= num_vertices {
168                return Err(format!(
169                    "arc {arc_index} endpoint out of range for {num_vertices} vertices"
170                )
171                .into());
172            }
173        }
174        for (edge_index, &(u, v)) in edges.iter().enumerate() {
175            if u >= num_vertices || v >= num_vertices {
176                return Err(format!(
177                    "edge {edge_index} endpoint out of range for {num_vertices} vertices"
178                )
179                .into());
180            }
181        }
182        for (arc_index, &length) in arc_lengths.iter().enumerate() {
183            if length < 0 {
184                return Err(format!("arc length {arc_index} must be nonnegative").into());
185            }
186        }
187        for (edge_index, &length) in edge_lengths.iter().enumerate() {
188            if length < 0 {
189                return Err(format!("edge length {edge_index} must be nonnegative").into());
190            }
191        }
192
193        Ok(Self {
194            num_vertices,
195            arcs,
196            edges,
197            arc_lengths,
198            edge_lengths,
199        })
200    }
201
202    /// Get the number of vertices in the mixed graph.
203    pub fn num_vertices(&self) -> usize {
204        self.num_vertices
205    }
206
207    /// Get the required directed arcs.
208    pub fn arcs(&self) -> &[(usize, usize)] {
209        &self.arcs
210    }
211
212    /// Get the available undirected edges.
213    pub fn edges(&self) -> &[(usize, usize)] {
214        &self.edges
215    }
216
217    /// Get the required arc lengths.
218    pub fn arc_lengths(&self) -> &[i64] {
219        &self.arc_lengths
220    }
221
222    /// Get the undirected edge lengths.
223    pub fn edge_lengths(&self) -> &[i64] {
224        &self.edge_lengths
225    }
226
227    /// Get the number of required arcs.
228    pub fn num_arcs(&self) -> usize {
229        self.arcs.len()
230    }
231
232    /// Get the number of undirected edges.
233    pub fn num_edges(&self) -> usize {
234        self.edges.len()
235    }
236
237    fn is_arc_permutation(&self, config: &[usize]) -> bool {
238        if config.len() != self.num_arcs() {
239            return false;
240        }
241
242        let mut seen = vec![false; self.num_arcs()];
243        for &arc_index in config {
244            if arc_index >= self.num_arcs() || seen[arc_index] {
245                return false;
246            }
247            seen[arc_index] = true;
248        }
249
250        true
251    }
252
253    fn mixed_graph_adjacency(&self) -> Vec<Vec<(usize, i64)>> {
254        let mut adjacency = vec![Vec::new(); self.num_vertices];
255
256        for (&(tail, head), &length) in self.arcs.iter().zip(&self.arc_lengths) {
257            adjacency[tail].push((head, length));
258        }
259
260        for (&(u, v), &length) in self.edges.iter().zip(&self.edge_lengths) {
261            adjacency[u].push((v, length));
262            adjacency[v].push((u, length));
263        }
264
265        adjacency
266    }
267
268    fn shortest_path_length(
269        &self,
270        adjacency: &[Vec<(usize, i64)>],
271        source: usize,
272        target: usize,
273    ) -> Option<i64> {
274        if source == target {
275            return Some(0);
276        }
277
278        let mut dist = vec![i64::MAX; self.num_vertices];
279        let mut heap = BinaryHeap::new();
280        dist[source] = 0;
281        heap.push((Reverse(0i64), source));
282
283        while let Some((Reverse(cost), node)) = heap.pop() {
284            if cost > dist[node] {
285                continue;
286            }
287            if node == target {
288                return Some(cost);
289            }
290
291            for &(next, length) in &adjacency[node] {
292                let next_cost = cost.checked_add(length)?;
293                if next_cost < dist[next] {
294                    dist[next] = next_cost;
295                    heap.push((Reverse(next_cost), next));
296                }
297            }
298        }
299
300        None
301    }
302
303    /// Compute the total closed-walk length induced by a configuration.
304    ///
305    /// Returns `None` for invalid permutations, unreachable connector paths,
306    /// or arithmetic overflow.
307    pub fn closed_walk_length(&self, config: &[usize]) -> Option<i64> {
308        if !self.is_arc_permutation(config) {
309            return None;
310        }
311        if config.is_empty() {
312            return Some(0);
313        }
314
315        let adjacency = self.mixed_graph_adjacency();
316        let mut total = 0i64;
317
318        for position in 0..config.len() {
319            let arc_index = config[position];
320            let next_arc_index = config[(position + 1) % config.len()];
321            let (_, arc_head) = self.arcs[arc_index];
322            let (next_arc_tail, _) = self.arcs[next_arc_index];
323
324            total = total.checked_add(self.arc_lengths[arc_index])?;
325            total = total.checked_add(self.shortest_path_length(
326                &adjacency,
327                arc_head,
328                next_arc_tail,
329            )?)?;
330        }
331
332        Some(total)
333    }
334}
335
336impl Problem for StackerCrane {
337    const NAME: &'static str = "StackerCrane";
338    type Solution = Vec<usize>;
339    type Value = Min<i64>;
340
341    crate::problem_parameters![
342        ("num_arcs", num_arcs),
343        ("num_edges", num_edges),
344        ("num_vertices", num_vertices),
345    ];
346
347    fn variant() -> Vec<(&'static str, &'static str)> {
348        crate::variant_params![]
349    }
350
351    fn evaluate(
352        &self,
353        config: &Self::Solution,
354    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
355        if config.len() != self.num_arcs() {
356            return Err(crate::traits::EvaluationError::InvalidConfiguration(
357                "arc ordering length does not match the required arcs".into(),
358            ));
359        }
360        if config.iter().any(|&arc| arc >= self.num_arcs()) {
361            return Err(crate::traits::EvaluationError::InvalidConfiguration(
362                "arc ordering contains an out-of-range arc".into(),
363            ));
364        }
365        Ok({
366            match self.closed_walk_length(config) {
367                Some(total) => Min(Some(total)),
368                None => Min(None),
369            }
370        })
371    }
372}
373
374impl crate::solvers::BruteForceProblem for StackerCrane {
375    fn dimensions(&self) -> Vec<usize> {
376        vec![self.num_arcs(); self.num_arcs()]
377    }
378}
379
380crate::declare_variants! {
381    default StackerCrane => "num_vertices^2 * 2^num_arcs" create StackerCraneCreateSpec,
382}
383
384crate::register_brute_force! {
385    StackerCrane,
386}
387
388#[derive(Debug, Clone, Deserialize)]
389struct StackerCraneDef {
390    num_vertices: usize,
391    arcs: Vec<(usize, usize)>,
392    edges: Vec<(usize, usize)>,
393    arc_lengths: Vec<i64>,
394    edge_lengths: Vec<i64>,
395}
396
397impl TryFrom<StackerCraneDef> for StackerCrane {
398    type Error = crate::registry::ConstructionError;
399
400    fn try_from(value: StackerCraneDef) -> Result<Self, Self::Error> {
401        Self::try_new(
402            value.num_vertices,
403            value.arcs,
404            value.edges,
405            value.arc_lengths,
406            value.edge_lengths,
407        )
408    }
409}
410
411#[cfg(feature = "example-db")]
412pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
413    vec![crate::example_db::specs::ModelExampleSpec {
414        id: "stacker_crane",
415        instance: Box::new(StackerCrane::new(
416            6,
417            vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)],
418            vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)],
419            vec![3, 4, 2, 5, 3],
420            vec![2, 1, 3, 2, 1, 4, 3],
421        )),
422        optimal_config: serde_json::json!(vec![0, 2, 1, 4, 3]),
423        optimal_value: serde_json::json!(20),
424    }]
425}
426
427#[cfg(test)]
428#[path = "../../unit_tests/models/misc/stacker_crane.rs"]
429mod tests;
430
431crate::decision_problem_meta!(StackerCrane, "DecisionStackerCrane");
432crate::register_decision_variant!(
433    StackerCrane, "DecisionStackerCrane", "num_vertices^2 * 2^num_arcs", &[],
434    "Does a feasible solution have objective value <= the bound?",
435    category: crate::registry::ProblemCategory::Misc,
436    dims: [],
437    fields: [
438        crate::registry::FieldInfo { name: "arcs", type_name: "Vec<(usize,usize)>", description: "Required directed arcs." },
439        crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "Undirected connector edges." },
440        crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "Vertex count, needed to preserve isolated vertices." },
441        crate::registry::FieldInfo { name: "arc_lengths", type_name: "Vec<i64>", description: "Required-arc lengths; defaults to one per arc." },
442        crate::registry::FieldInfo { name: "edge_lengths", type_name: "Vec<i64>", description: "Connector-edge lengths; defaults to one per edge." },
443        crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Accept objective values <= this bound" },
444    ],
445    decode: |_, indices: Vec<usize>| indices
446);
447
448#[cfg(feature = "example-db")]
449pub(crate) fn decision_canonical_rule_example_specs(
450) -> Vec<crate::example_db::specs::RuleExampleSpec> {
451    vec![crate::example_db::specs::RuleExampleSpec {
452        id: "decision_stacker_crane_to_stacker_crane",
453        build: || {
454            let source = crate::models::decision::Decision::new(
455                StackerCrane::new(
456                    6,
457                    vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)],
458                    vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)],
459                    vec![3, 4, 2, 5, 3],
460                    vec![2, 1, 3, 2, 1, 4, 3],
461                ),
462                20,
463            );
464            let witness = serde_json::json!(vec![0, 2, 1, 4, 3]);
465            crate::example_db::specs::rule_example_with_witness::<_, StackerCrane>(
466                source,
467                crate::export::SolutionPair {
468                    source_config: witness.clone(),
469                    target_config: witness,
470                },
471            )
472        },
473    }]
474}