Skip to main content

problemreductions/rules/
hamiltoniancircuit_stackercrane.rs

1//! Reduction from HamiltonianCircuit to StackerCrane.
2//!
3//! Vertex splitting into the mixed routing formulation of StackerCrane.
4//! Each vertex v_i is split into v_i^in (= 2i) and v_i^out (= 2i+1). A mandatory
5//! directed arc (v_i^in → v_i^out) of length 1 is added for each vertex. For each
6//! undirected edge {v_i, v_j} in the source graph, two undirected connector edges
7//! {v_i^out, v_j^in} and {v_j^out, v_i^in} of length 1 are added.
8//!
9//! The source graph has a Hamiltonian circuit iff the optimal Stacker Crane tour
10//! cost equals 2n and n >= 3 (n service arcs and n unit-cost connectors).
11//! Using connector length 1 (rather than 0) ensures that multi-hop connector
12//! paths cost strictly more than single-hop ones. Only permutations attaining
13//! this lower bound certify a Hamiltonian circuit.
14
15use crate::models::graph::HamiltonianCircuit;
16use crate::models::misc::StackerCrane;
17use crate::reduction;
18use crate::rules::traits::{ReduceTo, ReductionResult};
19use crate::topology::{Graph, SimpleGraph};
20
21/// Result of reducing HamiltonianCircuit to StackerCrane.
22#[derive(Debug, Clone)]
23pub struct ReductionHamiltonianCircuitToStackerCrane {
24    target: StackerCrane,
25}
26
27impl ReductionResult for ReductionHamiltonianCircuitToStackerCrane {
28    type Source = HamiltonianCircuit<SimpleGraph>;
29    type Target = StackerCrane;
30
31    fn target_problem(&self) -> &Self::Target {
32        &self.target
33    }
34
35    fn extract_solution(
36        &self,
37        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
38    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
39        let value =
40            crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
41        if !crate::rules::AggregateReductionResult::extract_value(self, value).0 {
42            return Err(crate::rules::ExtractionError::invalid(
43                "target tour does not certify a Hamiltonian circuit",
44            ));
45        }
46        // Service arc i corresponds to source vertex i.
47        Ok(target_solution.to_vec())
48    }
49}
50
51impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToStackerCrane {
52    type Source = HamiltonianCircuit<SimpleGraph>;
53    type Target = StackerCrane;
54
55    fn target_problem(&self) -> &Self::Target {
56        &self.target
57    }
58
59    fn extract_value(&self, value: crate::types::Min<i64>) -> crate::types::Or {
60        crate::types::Or(
61            self.target.num_arcs() >= 3
62                && value
63                    .0
64                    .is_some_and(|cost| usize::try_from(cost) == Ok(self.target.num_vertices())),
65        )
66    }
67}
68
69#[reduction(
70    aggregate = custom,
71    transform = exact {
72        num_vertices = "2 * num_vertices",
73        num_arcs = "num_vertices",
74        num_edges = "2 * num_edges",
75    }
76)]
77impl ReduceTo<StackerCrane> for HamiltonianCircuit<SimpleGraph> {
78    type Result = ReductionHamiltonianCircuitToStackerCrane;
79
80    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
81        let n = self.num_vertices();
82
83        // Each vertex i becomes two vertices: 2i (in) and 2i+1 (out).
84        let (target_num_vertices, target_num_edges) = split_graph_dimensions(n, self.num_edges())?;
85
86        // One mandatory arc per original vertex: (2i, 2i+1) with length 1.
87        let arcs: Vec<(usize, usize)> = (0..n).map(|i| (2 * i, 2 * i + 1)).collect();
88        let arc_lengths: Vec<i64> = vec![1; n];
89
90        // For each original edge {u, v}, add two undirected connector edges:
91        //   {u^out, v^in} = {2u+1, 2v}  with length 1
92        //   {v^out, u^in} = {2v+1, 2u}  with length 1
93        // Using length 1 (not 0) prevents multi-hop zero-cost shortcuts that
94        // would create optimal SC permutations not corresponding to valid HCs.
95        let mut edges = Vec::with_capacity(target_num_edges);
96        let mut edge_lengths = Vec::with_capacity(target_num_edges);
97        for (u, v) in self.graph().edges() {
98            edges.push((2 * u + 1, 2 * v));
99            edge_lengths.push(1);
100            edges.push((2 * v + 1, 2 * u));
101            edge_lengths.push(1);
102        }
103
104        let target =
105            StackerCrane::try_new(target_num_vertices, arcs, edges, arc_lengths, edge_lengths)
106                .map_err(<Self as ReduceTo<StackerCrane>>::target_construction)?;
107
108        Ok(ReductionHamiltonianCircuitToStackerCrane { target })
109    }
110}
111
112/// Check split indices and every finite service-order cost before allocation.
113fn split_graph_dimensions(
114    n: usize,
115    m: usize,
116) -> Result<(usize, usize), crate::rules::ReductionError> {
117    type Source = HamiltonianCircuit<SimpleGraph>;
118    let overflow = || {
119        crate::rules::ReductionError::integer_overflow::<Source, StackerCrane>(
120            "encoding split graph dimensions and route costs",
121        )
122    };
123    let vertices = n.checked_mul(2).ok_or_else(overflow)?;
124    let edges = m.checked_mul(2).ok_or_else(overflow)?;
125    // A shortest connector is simple and has at most 2n-1 unit steps.
126    // The n services therefore cost at most n * (1 + (2n-1)).
127    let cost_bound = n.checked_mul(vertices).ok_or_else(overflow)?;
128    <Source as ReduceTo<StackerCrane>>::exact_i64(cost_bound, "bounding split graph route costs")?;
129    Ok((vertices, edges))
130}
131
132#[cfg(feature = "example-db")]
133pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
134    use crate::export::SolutionPair;
135
136    vec![crate::example_db::specs::RuleExampleSpec {
137        id: "hamiltoniancircuit_to_stackercrane",
138        build: || {
139            let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
140            crate::example_db::specs::rule_example_with_witness::<_, StackerCrane>(
141                source,
142                SolutionPair {
143                    source_config: serde_json::json!(vec![0, 1, 2, 3]),
144                    target_config: serde_json::json!(vec![0, 1, 2, 3]),
145                },
146            )
147        },
148    }]
149}
150
151#[cfg(test)]
152#[path = "../unit_tests/rules/hamiltoniancircuit_stackercrane.rs"]
153mod tests;