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::{FieldInfo, 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        module_path: module_path!(),
21        description: "Find a closed walk that traverses each required directed arc and minimizes total length",
22        fields: &[
23            FieldInfo { name: "num_vertices", type_name: "usize", description: "Number of vertices in the mixed graph" },
24            FieldInfo { name: "arcs", type_name: "Vec<(usize, usize)>", description: "Required directed arcs that must be traversed" },
25            FieldInfo { name: "edges", type_name: "Vec<(usize, usize)>", description: "Undirected edges available for connector paths" },
26            FieldInfo { name: "arc_lengths", type_name: "Vec<i32>", description: "Nonnegative lengths of the required directed arcs" },
27            FieldInfo { name: "edge_lengths", type_name: "Vec<i32>", description: "Nonnegative lengths of the undirected connector edges" },
28        ],
29    }
30}
31
32/// The Stacker Crane problem.
33///
34/// A configuration is a permutation of the required arc indices. The walk
35/// traverses those arcs in the chosen order, connecting the head of each arc
36/// to the tail of the next arc by a shortest path in the mixed graph induced
37/// by the required directed arcs together with the undirected edges.
38/// The objective is to minimize the total walk length.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(try_from = "StackerCraneDef")]
41pub struct StackerCrane {
42    num_vertices: usize,
43    arcs: Vec<(usize, usize)>,
44    edges: Vec<(usize, usize)>,
45    arc_lengths: Vec<i32>,
46    edge_lengths: Vec<i32>,
47}
48
49impl StackerCrane {
50    /// Create a new Stacker Crane instance.
51    ///
52    /// # Panics
53    ///
54    /// Panics if the instance data are inconsistent or contain negative
55    /// lengths.
56    pub fn new(
57        num_vertices: usize,
58        arcs: Vec<(usize, usize)>,
59        edges: Vec<(usize, usize)>,
60        arc_lengths: Vec<i32>,
61        edge_lengths: Vec<i32>,
62    ) -> Self {
63        Self::try_new(num_vertices, arcs, edges, arc_lengths, edge_lengths)
64            .unwrap_or_else(|message| panic!("{message}"))
65    }
66
67    /// Create a new Stacker Crane instance, returning validation errors.
68    pub fn try_new(
69        num_vertices: usize,
70        arcs: Vec<(usize, usize)>,
71        edges: Vec<(usize, usize)>,
72        arc_lengths: Vec<i32>,
73        edge_lengths: Vec<i32>,
74    ) -> Result<Self, String> {
75        if arc_lengths.len() != arcs.len() {
76            return Err("arc_lengths length must match arcs length".to_string());
77        }
78        if edge_lengths.len() != edges.len() {
79            return Err("edge_lengths length must match edges length".to_string());
80        }
81        for (arc_index, &(tail, head)) in arcs.iter().enumerate() {
82            if tail >= num_vertices || head >= num_vertices {
83                return Err(format!(
84                    "arc {arc_index} endpoint out of range for {num_vertices} vertices"
85                ));
86            }
87        }
88        for (edge_index, &(u, v)) in edges.iter().enumerate() {
89            if u >= num_vertices || v >= num_vertices {
90                return Err(format!(
91                    "edge {edge_index} endpoint out of range for {num_vertices} vertices"
92                ));
93            }
94        }
95        for (arc_index, &length) in arc_lengths.iter().enumerate() {
96            if length < 0 {
97                return Err(format!("arc length {arc_index} must be nonnegative"));
98            }
99        }
100        for (edge_index, &length) in edge_lengths.iter().enumerate() {
101            if length < 0 {
102                return Err(format!("edge length {edge_index} must be nonnegative"));
103            }
104        }
105
106        Ok(Self {
107            num_vertices,
108            arcs,
109            edges,
110            arc_lengths,
111            edge_lengths,
112        })
113    }
114
115    /// Get the number of vertices in the mixed graph.
116    pub fn num_vertices(&self) -> usize {
117        self.num_vertices
118    }
119
120    /// Get the required directed arcs.
121    pub fn arcs(&self) -> &[(usize, usize)] {
122        &self.arcs
123    }
124
125    /// Get the available undirected edges.
126    pub fn edges(&self) -> &[(usize, usize)] {
127        &self.edges
128    }
129
130    /// Get the required arc lengths.
131    pub fn arc_lengths(&self) -> &[i32] {
132        &self.arc_lengths
133    }
134
135    /// Get the undirected edge lengths.
136    pub fn edge_lengths(&self) -> &[i32] {
137        &self.edge_lengths
138    }
139
140    /// Get the number of required arcs.
141    pub fn num_arcs(&self) -> usize {
142        self.arcs.len()
143    }
144
145    /// Get the number of undirected edges.
146    pub fn num_edges(&self) -> usize {
147        self.edges.len()
148    }
149
150    fn is_arc_permutation(&self, config: &[usize]) -> bool {
151        if config.len() != self.num_arcs() {
152            return false;
153        }
154
155        let mut seen = vec![false; self.num_arcs()];
156        for &arc_index in config {
157            if arc_index >= self.num_arcs() || seen[arc_index] {
158                return false;
159            }
160            seen[arc_index] = true;
161        }
162
163        true
164    }
165
166    fn mixed_graph_adjacency(&self) -> Vec<Vec<(usize, i32)>> {
167        let mut adjacency = vec![Vec::new(); self.num_vertices];
168
169        for (&(tail, head), &length) in self.arcs.iter().zip(&self.arc_lengths) {
170            adjacency[tail].push((head, length));
171        }
172
173        for (&(u, v), &length) in self.edges.iter().zip(&self.edge_lengths) {
174            adjacency[u].push((v, length));
175            adjacency[v].push((u, length));
176        }
177
178        adjacency
179    }
180
181    fn shortest_path_length(
182        &self,
183        adjacency: &[Vec<(usize, i32)>],
184        source: usize,
185        target: usize,
186    ) -> Option<i64> {
187        if source == target {
188            return Some(0);
189        }
190
191        let mut dist = vec![i64::MAX; self.num_vertices];
192        let mut heap = BinaryHeap::new();
193        dist[source] = 0;
194        heap.push((Reverse(0i64), source));
195
196        while let Some((Reverse(cost), node)) = heap.pop() {
197            if cost > dist[node] {
198                continue;
199            }
200            if node == target {
201                return Some(cost);
202            }
203
204            for &(next, length) in &adjacency[node] {
205                let next_cost = cost.checked_add(i64::from(length))?;
206                if next_cost < dist[next] {
207                    dist[next] = next_cost;
208                    heap.push((Reverse(next_cost), next));
209                }
210            }
211        }
212
213        None
214    }
215
216    /// Compute the total closed-walk length induced by a configuration.
217    ///
218    /// Returns `None` for invalid permutations, unreachable connector paths,
219    /// or arithmetic overflow.
220    pub fn closed_walk_length(&self, config: &[usize]) -> Option<i32> {
221        if !self.is_arc_permutation(config) {
222            return None;
223        }
224        if config.is_empty() {
225            return Some(0);
226        }
227
228        let adjacency = self.mixed_graph_adjacency();
229        let mut total = 0i64;
230
231        for position in 0..config.len() {
232            let arc_index = config[position];
233            let next_arc_index = config[(position + 1) % config.len()];
234            let (_, arc_head) = self.arcs[arc_index];
235            let (next_arc_tail, _) = self.arcs[next_arc_index];
236
237            total = total.checked_add(i64::from(self.arc_lengths[arc_index]))?;
238            total = total.checked_add(self.shortest_path_length(
239                &adjacency,
240                arc_head,
241                next_arc_tail,
242            )?)?;
243        }
244
245        i32::try_from(total).ok()
246    }
247}
248
249impl Problem for StackerCrane {
250    const NAME: &'static str = "StackerCrane";
251    type Value = Min<i32>;
252
253    fn variant() -> Vec<(&'static str, &'static str)> {
254        crate::variant_params![]
255    }
256
257    fn dims(&self) -> Vec<usize> {
258        vec![self.num_arcs(); self.num_arcs()]
259    }
260
261    fn evaluate(&self, config: &[usize]) -> Min<i32> {
262        match self.closed_walk_length(config) {
263            Some(total) => Min(Some(total)),
264            None => Min(None),
265        }
266    }
267}
268
269crate::declare_variants! {
270    default StackerCrane => "num_vertices^2 * 2^num_arcs",
271}
272
273#[derive(Debug, Clone, Deserialize)]
274struct StackerCraneDef {
275    num_vertices: usize,
276    arcs: Vec<(usize, usize)>,
277    edges: Vec<(usize, usize)>,
278    arc_lengths: Vec<i32>,
279    edge_lengths: Vec<i32>,
280}
281
282impl TryFrom<StackerCraneDef> for StackerCrane {
283    type Error = String;
284
285    fn try_from(value: StackerCraneDef) -> Result<Self, Self::Error> {
286        Self::try_new(
287            value.num_vertices,
288            value.arcs,
289            value.edges,
290            value.arc_lengths,
291            value.edge_lengths,
292        )
293    }
294}
295
296#[cfg(feature = "example-db")]
297pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
298    vec![crate::example_db::specs::ModelExampleSpec {
299        id: "stacker_crane",
300        instance: Box::new(StackerCrane::new(
301            6,
302            vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)],
303            vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)],
304            vec![3, 4, 2, 5, 3],
305            vec![2, 1, 3, 2, 1, 4, 3],
306        )),
307        optimal_config: vec![0, 2, 1, 4, 3],
308        optimal_value: serde_json::json!(20),
309    }]
310}
311
312#[cfg(test)]
313#[path = "../../unit_tests/models/misc/stacker_crane.rs"]
314mod tests;