Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -6680,6 +6680,44 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ Sort tasks by their completion times $C_j$ and encode that order back into the source schedule representation.
]

#let hc_tsp = load-example("HamiltonianCircuit", "TravelingSalesman")
#let hc_tsp_sol = hc_tsp.solutions.at(0)
#let hc_tsp_n = graph-num-vertices(hc_tsp.source.instance)
#let hc_tsp_source_edges = hc_tsp.source.instance.graph.edges
#let hc_tsp_target_edges = hc_tsp.target.instance.graph.edges
#let hc_tsp_target_weights = hc_tsp.target.instance.edge_weights
#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)))
#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)))
#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)))
#reduction-rule("HamiltonianCircuit", "TravelingSalesman",
example: true,
example-caption: [Cycle graph on $#hc_tsp_n$ vertices to weighted $K_#hc_tsp_n$],
extra: [
#pred-commands(
"pred create --example " + problem-spec(hc_tsp.source) + " -o hc.json",
"pred reduce hc.json --to " + target-spec(hc_tsp) + " -o bundle.json",
"pred solve bundle.json",
"pred evaluate hc.json --config " + hc_tsp_sol.source_config.map(str).join(","),
)

*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(", ")]$.\

*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.\

*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(", ")]$.\

*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.
],
)[
@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).
][
_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.

_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.

_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.
]

#let tsp_ilp = load-example("TravelingSalesman", "ILP")
#let tsp_ilp_sol = tsp_ilp.solutions.at(0)
#reduction-rule("TravelingSalesman", "ILP",
Expand Down
130 changes: 130 additions & 0 deletions src/rules/hamiltoniancircuit_travelingsalesman.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Reduction from HamiltonianCircuit to TravelingSalesman.
//!
//! The standard construction embeds the source graph into the complete graph on the
//! same vertex set, assigning weight 1 to source edges and weight 2 to non-edges.
//! The target optimum is exactly n iff the source graph contains a Hamiltonian circuit.

use crate::models::graph::{HamiltonianCircuit, TravelingSalesman};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{Graph, SimpleGraph};

/// Result of reducing HamiltonianCircuit to TravelingSalesman.
#[derive(Debug, Clone)]
pub struct ReductionHamiltonianCircuitToTravelingSalesman {
target: TravelingSalesman<SimpleGraph, i32>,
}

impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman {
type Source = HamiltonianCircuit<SimpleGraph>;
type Target = TravelingSalesman<SimpleGraph, i32>;

fn target_problem(&self) -> &Self::Target {
&self.target
}

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
let graph = self.target.graph();
let n = graph.num_vertices();
if n == 0 {
return vec![];
}

let edges = graph.edges();
if target_solution.len() != edges.len() {
return vec![0; n];
}

let mut adjacency = vec![Vec::new(); n];
let mut selected_count = 0usize;
for (idx, &selected) in target_solution.iter().enumerate() {
if selected != 1 {
continue;
}
let (u, v) = edges[idx];
adjacency[u].push(v);
adjacency[v].push(u);
selected_count += 1;
}

if selected_count != n || adjacency.iter().any(|neighbors| neighbors.len() != 2) {
return vec![0; n];
}

for neighbors in &mut adjacency {
neighbors.sort_unstable();
}

let mut order = Vec::with_capacity(n);
let mut prev = None;
let mut current = 0usize;

for _ in 0..n {
order.push(current);
let neighbors = &adjacency[current];
let next = match prev {
Some(previous) => {
if neighbors[0] == previous {
neighbors[1]
} else {
neighbors[0]
}
}
None => neighbors[0],
};
prev = Some(current);
current = next;
}

order
}
}

#[reduction(
overhead = {
num_vertices = "num_vertices",
num_edges = "num_vertices * (num_vertices - 1) / 2",
}
)]
impl ReduceTo<TravelingSalesman<SimpleGraph, i32>> for HamiltonianCircuit<SimpleGraph> {
type Result = ReductionHamiltonianCircuitToTravelingSalesman;

fn reduce_to(&self) -> Self::Result {
let num_vertices = self.num_vertices();
let target_graph = SimpleGraph::complete(num_vertices);
let weights = target_graph
.edges()
.into_iter()
.map(|(u, v)| if self.graph().has_edge(u, v) { 1 } else { 2 })
.collect();
let target = TravelingSalesman::new(target_graph, weights);

ReductionHamiltonianCircuitToTravelingSalesman { target }
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "hamiltoniancircuit_to_travelingsalesman",
build: || {
let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
crate::example_db::specs::rule_example_with_witness::<
_,
TravelingSalesman<SimpleGraph, i32>,
>(
source,
SolutionPair {
source_config: vec![0, 1, 2, 3],
target_config: vec![1, 0, 1, 1, 0, 1],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs"]
mod tests;
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) mod factoring_circuit;
mod graph;
pub(crate) mod graphpartitioning_maxcut;
pub(crate) mod graphpartitioning_qubo;
pub(crate) mod hamiltoniancircuit_travelingsalesman;
mod kcoloring_casts;
mod knapsack_qubo;
mod ksatisfiability_casts;
Expand Down Expand Up @@ -105,6 +106,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(factoring_circuit::canonical_rule_example_specs());
specs.extend(graphpartitioning_maxcut::canonical_rule_example_specs());
specs.extend(graphpartitioning_qubo::canonical_rule_example_specs());
specs.extend(hamiltoniancircuit_travelingsalesman::canonical_rule_example_specs());
specs.extend(knapsack_qubo::canonical_rule_example_specs());
specs.extend(ksatisfiability_qubo::canonical_rule_example_specs());
specs.extend(ksatisfiability_subsetsum::canonical_rule_example_specs());
Expand Down
74 changes: 74 additions & 0 deletions src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use crate::models::graph::{HamiltonianCircuit, TravelingSalesman};
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target;
use crate::rules::ReduceTo;
use crate::rules::ReductionResult;
use crate::solvers::{BruteForce, Solver};
use crate::topology::{Graph, SimpleGraph};
use crate::types::SolutionSize;
use crate::Problem;

fn cycle4_hc() -> HamiltonianCircuit<SimpleGraph> {
HamiltonianCircuit::new(SimpleGraph::cycle(4))
}

#[test]
fn test_hamiltoniancircuit_to_travelingsalesman_closed_loop() {
let source = cycle4_hc();
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);

assert_satisfaction_round_trip_from_optimization_target(
&source,
&reduction,
"HamiltonianCircuit -> TravelingSalesman",
);
}

#[test]
fn test_hamiltoniancircuit_to_travelingsalesman_structure() {
let source = cycle4_hc();
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
let target = reduction.target_problem();

assert_eq!(target.graph().num_vertices(), 4);
assert_eq!(target.graph().num_edges(), 6);

for ((u, v), weight) in target.graph().edges().into_iter().zip(target.weights()) {
let expected = if source.graph().has_edge(u, v) { 1 } else { 2 };
assert_eq!(weight, expected, "unexpected weight on edge ({u}, {v})");
}
}

#[test]
fn test_hamiltoniancircuit_to_travelingsalesman_nonhamiltonian_cost_gap() {
let source = HamiltonianCircuit::new(SimpleGraph::star(4));
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
let target = reduction.target_problem();
let best = BruteForce::new()
.find_best(target)
.expect("complete weighted graph should always admit a tour");

match target.evaluate(&best) {
SolutionSize::Valid(cost) => assert!(cost > 4, "expected cost > 4, got {cost}"),
SolutionSize::Invalid => panic!("best TSP solution evaluated as invalid"),
}
}

#[test]
fn test_hamiltoniancircuit_to_travelingsalesman_extract_solution_cycle() {
let source = cycle4_hc();
let reduction = ReduceTo::<TravelingSalesman<SimpleGraph, i32>>::reduce_to(&source);
let target = reduction.target_problem();
let cycle_edges = [(0usize, 1usize), (1, 2), (2, 3), (0, 3)];
let target_solution: Vec<usize> = target
.graph()
.edges()
.into_iter()
.map(|(u, v)| usize::from(cycle_edges.contains(&(u, v)) || cycle_edges.contains(&(v, u))))
.collect();

let extracted = reduction.extract_solution(&target_solution);

assert_eq!(target.evaluate(&target_solution), SolutionSize::Valid(4));
assert_eq!(extracted.len(), 4);
assert!(source.evaluate(&extracted));
}
Loading