From 1ed4583d1f6512b5e28aec92c11beb15b2dbef07 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sat, 28 Mar 2026 00:42:17 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20#789:=20HC=E2=86=92StackerCrane=20reducti?= =?UTF-8?q?on=20returns=20incorrect=20result=20on=20prism=20graph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-cost connector edges allowed multi-hop shortest paths between non-adjacent arcs, creating optimal SC permutations that don't map back to valid Hamiltonian circuits. Change connector edge lengths from 0 to 1 so only single-hop connectors are optimal (cost 2n instead of n). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/rules/hamiltoniancircuit_stackercrane.rs | 18 +++++---- .../rules/hamiltoniancircuit_stackercrane.rs | 40 +++++++++++++++---- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/rules/hamiltoniancircuit_stackercrane.rs b/src/rules/hamiltoniancircuit_stackercrane.rs index 88bdaf20..7b8d0534 100644 --- a/src/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/rules/hamiltoniancircuit_stackercrane.rs @@ -4,11 +4,13 @@ //! Each vertex v_i is split into v_i^in (= 2i) and v_i^out (= 2i+1). A mandatory //! directed arc (v_i^in → v_i^out) of length 1 is added for each vertex. For each //! undirected edge {v_i, v_j} in the source graph, two undirected connector edges -//! {v_i^out, v_j^in} and {v_j^out, v_i^in} of length 0 are added. +//! {v_i^out, v_j^in} and {v_j^out, v_i^in} of length 1 are added. //! //! The source graph has a Hamiltonian circuit iff the optimal Stacker Crane tour -//! cost equals n (the number of vertices), since each arc contributes cost 1 and -//! each zero-cost connector edge links consecutive arcs for free. +//! cost equals 2n (n arcs of cost 1 plus n single-hop connectors of cost 1). +//! Using connector length 1 (rather than 0) ensures that multi-hop connector +//! paths cost strictly more than single-hop ones, so every optimal permutation +//! corresponds to a valid Hamiltonian circuit. use crate::models::graph::HamiltonianCircuit; use crate::models::misc::StackerCrane; @@ -59,15 +61,17 @@ impl ReduceTo for HamiltonianCircuit { let arc_lengths: Vec = vec![1; n]; // For each original edge {u, v}, add two undirected connector edges: - // {u^out, v^in} = {2u+1, 2v} with length 0 - // {v^out, u^in} = {2v+1, 2u} with length 0 + // {u^out, v^in} = {2u+1, 2v} with length 1 + // {v^out, u^in} = {2v+1, 2u} with length 1 + // Using length 1 (not 0) prevents multi-hop zero-cost shortcuts that + // would create optimal SC permutations not corresponding to valid HCs. let mut edges = Vec::new(); let mut edge_lengths = Vec::new(); for (u, v) in self.graph().edges() { edges.push((2 * u + 1, 2 * v)); - edge_lengths.push(0); + edge_lengths.push(1); edges.push((2 * v + 1, 2 * u)); - edge_lengths.push(0); + edge_lengths.push(1); } let target = StackerCrane::new(target_num_vertices, arcs, edges, arc_lengths, edge_lengths); diff --git a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs index b3032f96..264cbbb3 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs @@ -41,15 +41,15 @@ fn test_hamiltoniancircuit_to_stackercrane_structure() { for &len in target.arc_lengths() { assert_eq!(len, 1); } - // All edges have length 0 + // All connector edges have length 1 for &len in target.edge_lengths() { - assert_eq!(len, 0); + assert_eq!(len, 1); } } #[test] fn test_hamiltoniancircuit_to_stackercrane_optimal_cost() { - // A 4-cycle has a Hamiltonian circuit; optimal StackerCrane cost = 4. + // A 4-cycle has a Hamiltonian circuit; optimal StackerCrane cost = 2n = 8. let source = cycle4_hc(); let reduction = ReduceTo::::reduce_to(&source); let target = reduction.target_problem(); @@ -58,13 +58,13 @@ fn test_hamiltoniancircuit_to_stackercrane_optimal_cost() { .find_witness(target) .expect("target should have a solution"); let cost = target.evaluate(&witness); - assert_eq!(cost, Min(Some(4))); + assert_eq!(cost, Min(Some(8))); } #[test] fn test_hamiltoniancircuit_to_stackercrane_non_hamiltonian() { // Star graph on 4 vertices: no Hamiltonian circuit. - // The optimal StackerCrane cost should exceed n = 4. + // The optimal StackerCrane cost should exceed 2n = 8. let source = HamiltonianCircuit::new(SimpleGraph::star(4)); let reduction = ReduceTo::::reduce_to(&source); let target = reduction.target_problem(); @@ -74,8 +74,8 @@ fn test_hamiltoniancircuit_to_stackercrane_non_hamiltonian() { Some(w) => { let cost = target.evaluate(&w); assert!( - cost.0.unwrap() > 4, - "non-Hamiltonian graph should have cost > n" + cost.0.unwrap() > 8, + "non-Hamiltonian graph should have cost > 2n" ); } None => { @@ -99,3 +99,29 @@ fn test_hamiltoniancircuit_to_stackercrane_extract_solution() { "extracted solution should be a valid HC" ); } + +#[test] +fn test_hamiltoniancircuit_to_stackercrane_prism_graph() { + // Regression test for #789: prism graph (6 vertices, 9 edges) has a + // Hamiltonian circuit, but with zero-cost connectors the ILP could find + // an optimal SC permutation that doesn't correspond to a valid HC. + let edges = vec![ + (0, 1), + (1, 2), + (2, 0), + (3, 4), + (4, 5), + (5, 3), + (0, 3), + (1, 4), + (2, 5), + ]; + let source = HamiltonianCircuit::new(SimpleGraph::new(6, edges)); + let reduction = ReduceTo::::reduce_to(&source); + + assert_satisfaction_round_trip_from_optimization_target( + &source, + &reduction, + "HamiltonianCircuit -> StackerCrane (prism graph)", + ); +}