Skip to content

Commit e1b41bd

Browse files
isPANNclaude
andauthored
Fix #789: HC→StackerCrane reduction returns incorrect result on prism graph (#791)
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) <noreply@anthropic.com>
1 parent 4bed271 commit e1b41bd

File tree

2 files changed

+44
-14
lines changed

2 files changed

+44
-14
lines changed

src/rules/hamiltoniancircuit_stackercrane.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
//! Each vertex v_i is split into v_i^in (= 2i) and v_i^out (= 2i+1). A mandatory
55
//! directed arc (v_i^in → v_i^out) of length 1 is added for each vertex. For each
66
//! 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 0 are added.
7+
//! {v_i^out, v_j^in} and {v_j^out, v_i^in} of length 1 are added.
88
//!
99
//! The source graph has a Hamiltonian circuit iff the optimal Stacker Crane tour
10-
//! cost equals n (the number of vertices), since each arc contributes cost 1 and
11-
//! each zero-cost connector edge links consecutive arcs for free.
10+
//! cost equals 2n (n arcs of cost 1 plus n single-hop connectors of cost 1).
11+
//! Using connector length 1 (rather than 0) ensures that multi-hop connector
12+
//! paths cost strictly more than single-hop ones, so every optimal permutation
13+
//! corresponds to a valid Hamiltonian circuit.
1214
1315
use crate::models::graph::HamiltonianCircuit;
1416
use crate::models::misc::StackerCrane;
@@ -59,15 +61,17 @@ impl ReduceTo<StackerCrane> for HamiltonianCircuit<SimpleGraph> {
5961
let arc_lengths: Vec<i32> = vec![1; n];
6062

6163
// For each original edge {u, v}, add two undirected connector edges:
62-
// {u^out, v^in} = {2u+1, 2v} with length 0
63-
// {v^out, u^in} = {2v+1, 2u} with length 0
64+
// {u^out, v^in} = {2u+1, 2v} with length 1
65+
// {v^out, u^in} = {2v+1, 2u} with length 1
66+
// Using length 1 (not 0) prevents multi-hop zero-cost shortcuts that
67+
// would create optimal SC permutations not corresponding to valid HCs.
6468
let mut edges = Vec::new();
6569
let mut edge_lengths = Vec::new();
6670
for (u, v) in self.graph().edges() {
6771
edges.push((2 * u + 1, 2 * v));
68-
edge_lengths.push(0);
72+
edge_lengths.push(1);
6973
edges.push((2 * v + 1, 2 * u));
70-
edge_lengths.push(0);
74+
edge_lengths.push(1);
7175
}
7276

7377
let target = StackerCrane::new(target_num_vertices, arcs, edges, arc_lengths, edge_lengths);

src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ fn test_hamiltoniancircuit_to_stackercrane_structure() {
4141
for &len in target.arc_lengths() {
4242
assert_eq!(len, 1);
4343
}
44-
// All edges have length 0
44+
// All connector edges have length 1
4545
for &len in target.edge_lengths() {
46-
assert_eq!(len, 0);
46+
assert_eq!(len, 1);
4747
}
4848
}
4949

5050
#[test]
5151
fn test_hamiltoniancircuit_to_stackercrane_optimal_cost() {
52-
// A 4-cycle has a Hamiltonian circuit; optimal StackerCrane cost = 4.
52+
// A 4-cycle has a Hamiltonian circuit; optimal StackerCrane cost = 2n = 8.
5353
let source = cycle4_hc();
5454
let reduction = ReduceTo::<StackerCrane>::reduce_to(&source);
5555
let target = reduction.target_problem();
@@ -58,13 +58,13 @@ fn test_hamiltoniancircuit_to_stackercrane_optimal_cost() {
5858
.find_witness(target)
5959
.expect("target should have a solution");
6060
let cost = target.evaluate(&witness);
61-
assert_eq!(cost, Min(Some(4)));
61+
assert_eq!(cost, Min(Some(8)));
6262
}
6363

6464
#[test]
6565
fn test_hamiltoniancircuit_to_stackercrane_non_hamiltonian() {
6666
// Star graph on 4 vertices: no Hamiltonian circuit.
67-
// The optimal StackerCrane cost should exceed n = 4.
67+
// The optimal StackerCrane cost should exceed 2n = 8.
6868
let source = HamiltonianCircuit::new(SimpleGraph::star(4));
6969
let reduction = ReduceTo::<StackerCrane>::reduce_to(&source);
7070
let target = reduction.target_problem();
@@ -74,8 +74,8 @@ fn test_hamiltoniancircuit_to_stackercrane_non_hamiltonian() {
7474
Some(w) => {
7575
let cost = target.evaluate(&w);
7676
assert!(
77-
cost.0.unwrap() > 4,
78-
"non-Hamiltonian graph should have cost > n"
77+
cost.0.unwrap() > 8,
78+
"non-Hamiltonian graph should have cost > 2n"
7979
);
8080
}
8181
None => {
@@ -99,3 +99,29 @@ fn test_hamiltoniancircuit_to_stackercrane_extract_solution() {
9999
"extracted solution should be a valid HC"
100100
);
101101
}
102+
103+
#[test]
104+
fn test_hamiltoniancircuit_to_stackercrane_prism_graph() {
105+
// Regression test for #789: prism graph (6 vertices, 9 edges) has a
106+
// Hamiltonian circuit, but with zero-cost connectors the ILP could find
107+
// an optimal SC permutation that doesn't correspond to a valid HC.
108+
let edges = vec![
109+
(0, 1),
110+
(1, 2),
111+
(2, 0),
112+
(3, 4),
113+
(4, 5),
114+
(5, 3),
115+
(0, 3),
116+
(1, 4),
117+
(2, 5),
118+
];
119+
let source = HamiltonianCircuit::new(SimpleGraph::new(6, edges));
120+
let reduction = ReduceTo::<StackerCrane>::reduce_to(&source);
121+
122+
assert_satisfaction_round_trip_from_optimization_target(
123+
&source,
124+
&reduction,
125+
"HamiltonianCircuit -> StackerCrane (prism graph)",
126+
);
127+
}

0 commit comments

Comments
 (0)