Skip to content

Commit 29ae20f

Browse files
authored
Fix #258: [Rule] HamiltonianCircuit to TravelingSalesman (#745)
* Add plan for #258: [Rule] HamiltonianCircuit to TravelingSalesman * Implement #258: [Rule] HamiltonianCircuit to TravelingSalesman * chore: remove plan file after implementation
1 parent ca7b5b6 commit 29ae20f

4 files changed

Lines changed: 244 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6722,6 +6722,44 @@ The following reductions to Integer Linear Programming are straightforward formu
67226722
_Solution extraction._ Sort tasks by their completion times $C_j$ and encode that order back into the source schedule representation.
67236723
]
67246724

6725+
#let hc_tsp = load-example("HamiltonianCircuit", "TravelingSalesman")
6726+
#let hc_tsp_sol = hc_tsp.solutions.at(0)
6727+
#let hc_tsp_n = graph-num-vertices(hc_tsp.source.instance)
6728+
#let hc_tsp_source_edges = hc_tsp.source.instance.graph.edges
6729+
#let hc_tsp_target_edges = hc_tsp.target.instance.graph.edges
6730+
#let hc_tsp_target_weights = hc_tsp.target.instance.edge_weights
6731+
#let hc_tsp_weight_one = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_target_weights.at(i) == 1).map(((i, e)) => (e.at(0), e.at(1)))
6732+
#let hc_tsp_weight_two = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_target_weights.at(i) == 2).map(((i, e)) => (e.at(0), e.at(1)))
6733+
#let hc_tsp_selected_edges = hc_tsp_target_edges.enumerate().filter(((i, _)) => hc_tsp_sol.target_config.at(i) == 1).map(((i, e)) => (e.at(0), e.at(1)))
6734+
#reduction-rule("HamiltonianCircuit", "TravelingSalesman",
6735+
example: true,
6736+
example-caption: [Cycle graph on $#hc_tsp_n$ vertices to weighted $K_#hc_tsp_n$],
6737+
extra: [
6738+
#pred-commands(
6739+
"pred create --example " + problem-spec(hc_tsp.source) + " -o hc.json",
6740+
"pred reduce hc.json --to " + target-spec(hc_tsp) + " -o bundle.json",
6741+
"pred solve bundle.json",
6742+
"pred evaluate hc.json --config " + hc_tsp_sol.source_config.map(str).join(","),
6743+
)
6744+
6745+
*Step 1 -- Start from the source graph.* The canonical source fixture is the cycle on vertices ${0, 1, 2, 3}$ with edges #hc_tsp_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#hc_tsp_sol.source_config.map(str).join(", ")]$.\
6746+
6747+
*Step 2 -- Complete the graph and encode adjacency by weights.* The target keeps the same $#hc_tsp_n$ vertices but adds the missing diagonals, so it becomes $K_#hc_tsp_n$ with $#graph-num-edges(hc_tsp.target.instance)$ undirected edges. The original cycle edges #hc_tsp_weight_one.map(e => $(#e.at(0), #e.at(1))$).join(", ") receive weight 1, while the diagonals #hc_tsp_weight_two.map(e => $(#e.at(0), #e.at(1))$).join(", ") receive weight 2.\
6748+
6749+
*Step 3 -- Verify the canonical witness.* The stored target configuration $[#hc_tsp_sol.target_config.map(str).join(", ")]$ selects the tour edges #hc_tsp_selected_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). Its total cost is $1 + 1 + 1 + 1 = #hc_tsp_n$, so every chosen edge is a weight-1 source edge, and traversing the selected cycle recovers the Hamiltonian circuit $[#hc_tsp_sol.source_config.map(str).join(", ")]$.\
6750+
6751+
*Multiplicity:* The fixture stores one canonical witness. For the 4-cycle there are $4 times 2 = 8$ Hamiltonian-circuit permutations (choice of start vertex and direction), but they all induce the same undirected target edge set.
6752+
],
6753+
)[
6754+
@garey1979 This $O(n^2)$ reduction constructs the complete graph on the same vertex set and uses edge weights to distinguish source edges from non-edges: weight 1 means "present in the source" and weight 2 means "missing in the source" ($n (n - 1) / 2$ target edges).
6755+
][
6756+
_Construction._ Given a Hamiltonian Circuit instance $G = (V, E)$ with $n = |V|$, construct the complete graph $K_n$ on the same vertex set. For each pair $u < v$, set $w(u, v) = 1$ if $(u, v) in E$ and $w(u, v) = 2$ otherwise. The target TSP instance asks for a minimum-weight Hamiltonian cycle in this weighted complete graph.
6757+
6758+
_Correctness._ ($arrow.r.double$) If $G$ has a Hamiltonian circuit $v_0, v_1, dots, v_(n-1), v_0$, then the same cycle exists in $K_n$. Every chosen edge belongs to $E$, so each edge has weight 1 and the resulting TSP tour has total cost $n$. ($arrow.l.double$) Every TSP tour on $n$ vertices uses exactly $n$ edges, and every target edge has weight at least 1, so any tour has cost at least $n$. If the optimum cost is exactly $n$, every selected edge must therefore have weight 1. Those edges are precisely edges of $G$, so the optimal TSP tour is already a Hamiltonian circuit in the source graph.
6759+
6760+
_Solution extraction._ Read the selected TSP edges, traverse the unique degree-2 cycle they form, and return the resulting vertex permutation as the source Hamiltonian-circuit witness.
6761+
]
6762+
67256763
#let tsp_ilp = load-example("TravelingSalesman", "ILP")
67266764
#let tsp_ilp_sol = tsp_ilp.solutions.at(0)
67276765
#reduction-rule("TravelingSalesman", "ILP",
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! Reduction from HamiltonianCircuit to TravelingSalesman.
2+
//!
3+
//! The standard construction embeds the source graph into the complete graph on the
4+
//! same vertex set, assigning weight 1 to source edges and weight 2 to non-edges.
5+
//! The target optimum is exactly n iff the source graph contains a Hamiltonian circuit.
6+
7+
use crate::models::graph::{HamiltonianCircuit, TravelingSalesman};
8+
use crate::reduction;
9+
use crate::rules::traits::{ReduceTo, ReductionResult};
10+
use crate::topology::{Graph, SimpleGraph};
11+
12+
/// Result of reducing HamiltonianCircuit to TravelingSalesman.
13+
#[derive(Debug, Clone)]
14+
pub struct ReductionHamiltonianCircuitToTravelingSalesman {
15+
target: TravelingSalesman<SimpleGraph, i32>,
16+
}
17+
18+
impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman {
19+
type Source = HamiltonianCircuit<SimpleGraph>;
20+
type Target = TravelingSalesman<SimpleGraph, i32>;
21+
22+
fn target_problem(&self) -> &Self::Target {
23+
&self.target
24+
}
25+
26+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
27+
let graph = self.target.graph();
28+
let n = graph.num_vertices();
29+
if n == 0 {
30+
return vec![];
31+
}
32+
33+
let edges = graph.edges();
34+
if target_solution.len() != edges.len() {
35+
return vec![0; n];
36+
}
37+
38+
let mut adjacency = vec![Vec::new(); n];
39+
let mut selected_count = 0usize;
40+
for (idx, &selected) in target_solution.iter().enumerate() {
41+
if selected != 1 {
42+
continue;
43+
}
44+
let (u, v) = edges[idx];
45+
adjacency[u].push(v);
46+
adjacency[v].push(u);
47+
selected_count += 1;
48+
}
49+
50+
if selected_count != n || adjacency.iter().any(|neighbors| neighbors.len() != 2) {
51+
return vec![0; n];
52+
}
53+
54+
for neighbors in &mut adjacency {
55+
neighbors.sort_unstable();
56+
}
57+
58+
let mut order = Vec::with_capacity(n);
59+
let mut prev = None;
60+
let mut current = 0usize;
61+
62+
for _ in 0..n {
63+
order.push(current);
64+
let neighbors = &adjacency[current];
65+
let next = match prev {
66+
Some(previous) => {
67+
if neighbors[0] == previous {
68+
neighbors[1]
69+
} else {
70+
neighbors[0]
71+
}
72+
}
73+
None => neighbors[0],
74+
};
75+
prev = Some(current);
76+
current = next;
77+
}
78+
79+
order
80+
}
81+
}
82+
83+
#[reduction(
84+
overhead = {
85+
num_vertices = "num_vertices",
86+
num_edges = "num_vertices * (num_vertices - 1) / 2",
87+
}
88+
)]
89+
impl ReduceTo<TravelingSalesman<SimpleGraph, i32>> for HamiltonianCircuit<SimpleGraph> {
90+
type Result = ReductionHamiltonianCircuitToTravelingSalesman;
91+
92+
fn reduce_to(&self) -> Self::Result {
93+
let num_vertices = self.num_vertices();
94+
let target_graph = SimpleGraph::complete(num_vertices);
95+
let weights = target_graph
96+
.edges()
97+
.into_iter()
98+
.map(|(u, v)| if self.graph().has_edge(u, v) { 1 } else { 2 })
99+
.collect();
100+
let target = TravelingSalesman::new(target_graph, weights);
101+
102+
ReductionHamiltonianCircuitToTravelingSalesman { target }
103+
}
104+
}
105+
106+
#[cfg(feature = "example-db")]
107+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
108+
use crate::export::SolutionPair;
109+
110+
vec![crate::example_db::specs::RuleExampleSpec {
111+
id: "hamiltoniancircuit_to_travelingsalesman",
112+
build: || {
113+
let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
114+
crate::example_db::specs::rule_example_with_witness::<
115+
_,
116+
TravelingSalesman<SimpleGraph, i32>,
117+
>(
118+
source,
119+
SolutionPair {
120+
source_config: vec![0, 1, 2, 3],
121+
target_config: vec![1, 0, 1, 1, 0, 1],
122+
},
123+
)
124+
},
125+
}]
126+
}
127+
128+
#[cfg(test)]
129+
#[path = "../unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs"]
130+
mod tests;

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(crate) mod factoring_circuit;
1313
mod graph;
1414
pub(crate) mod graphpartitioning_maxcut;
1515
pub(crate) mod graphpartitioning_qubo;
16+
pub(crate) mod hamiltoniancircuit_travelingsalesman;
1617
mod kcoloring_casts;
1718
mod knapsack_qubo;
1819
mod ksatisfiability_casts;
@@ -106,6 +107,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
106107
specs.extend(factoring_circuit::canonical_rule_example_specs());
107108
specs.extend(graphpartitioning_maxcut::canonical_rule_example_specs());
108109
specs.extend(graphpartitioning_qubo::canonical_rule_example_specs());
110+
specs.extend(hamiltoniancircuit_travelingsalesman::canonical_rule_example_specs());
109111
specs.extend(knapsack_qubo::canonical_rule_example_specs());
110112
specs.extend(ksatisfiability_qubo::canonical_rule_example_specs());
111113
specs.extend(ksatisfiability_subsetsum::canonical_rule_example_specs());
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use crate::models::graph::{HamiltonianCircuit, TravelingSalesman};
2+
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target;
3+
use crate::rules::ReduceTo;
4+
use crate::rules::ReductionResult;
5+
use crate::solvers::{BruteForce, Solver};
6+
use crate::topology::{Graph, SimpleGraph};
7+
use crate::types::SolutionSize;
8+
use crate::Problem;
9+
10+
fn cycle4_hc() -> HamiltonianCircuit<SimpleGraph> {
11+
HamiltonianCircuit::new(SimpleGraph::cycle(4))
12+
}
13+
14+
#[test]
15+
fn test_hamiltoniancircuit_to_travelingsalesman_closed_loop() {
16+
let source = cycle4_hc();
17+
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
18+
19+
assert_satisfaction_round_trip_from_optimization_target(
20+
&source,
21+
&reduction,
22+
"HamiltonianCircuit -> TravelingSalesman",
23+
);
24+
}
25+
26+
#[test]
27+
fn test_hamiltoniancircuit_to_travelingsalesman_structure() {
28+
let source = cycle4_hc();
29+
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
30+
let target = reduction.target_problem();
31+
32+
assert_eq!(target.graph().num_vertices(), 4);
33+
assert_eq!(target.graph().num_edges(), 6);
34+
35+
for ((u, v), weight) in target.graph().edges().into_iter().zip(target.weights()) {
36+
let expected = if source.graph().has_edge(u, v) { 1 } else { 2 };
37+
assert_eq!(weight, expected, "unexpected weight on edge ({u}, {v})");
38+
}
39+
}
40+
41+
#[test]
42+
fn test_hamiltoniancircuit_to_travelingsalesman_nonhamiltonian_cost_gap() {
43+
let source = HamiltonianCircuit::new(SimpleGraph::star(4));
44+
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
45+
let target = reduction.target_problem();
46+
let best = BruteForce::new()
47+
.find_best(target)
48+
.expect("complete weighted graph should always admit a tour");
49+
50+
match target.evaluate(&best) {
51+
SolutionSize::Valid(cost) => assert!(cost > 4, "expected cost > 4, got {cost}"),
52+
SolutionSize::Invalid => panic!("best TSP solution evaluated as invalid"),
53+
}
54+
}
55+
56+
#[test]
57+
fn test_hamiltoniancircuit_to_travelingsalesman_extract_solution_cycle() {
58+
let source = cycle4_hc();
59+
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
60+
let target = reduction.target_problem();
61+
let cycle_edges = [(0usize, 1usize), (1, 2), (2, 3), (0, 3)];
62+
let target_solution: Vec<usize> = target
63+
.graph()
64+
.edges()
65+
.into_iter()
66+
.map(|(u, v)| usize::from(cycle_edges.contains(&(u, v)) || cycle_edges.contains(&(v, u))))
67+
.collect();
68+
69+
let extracted = reduction.extract_solution(&target_solution);
70+
71+
assert_eq!(target.evaluate(&target_solution), SolutionSize::Valid(4));
72+
assert_eq!(extracted.len(), 4);
73+
assert!(source.evaluate(&extracted));
74+
}

0 commit comments

Comments
 (0)