Skip to content
Open
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
43 changes: 43 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -17082,6 +17082,49 @@ The following table shows concrete variable overhead for example instances, take
_Solution extraction._ For each source variable $x_i$, compute $x_i = sum_(j=0)^(K_i - 1) w_(i j) y_(i j)$ from the binary solution.
]

#let hp_hc = load-example("HamiltonianPath", "HamiltonianCircuit")
#let hp_hc_sol = hp_hc.solutions.at(0)
#let hp_hc_n = graph-num-vertices(hp_hc.source.instance)
#let hp_hc_m = graph-num-edges(hp_hc.source.instance)
#let hp_hc_source_edges = hp_hc.source.instance.graph.edges
#let hp_hc_target_n = graph-num-vertices(hp_hc.target.instance)
#let hp_hc_target_edges = hp_hc.target.instance.graph.edges
#let hp_hc_x = hp_hc_n
#reduction-rule("HamiltonianPath", "HamiltonianCircuit",
example: true,
example-caption: [Add universal vertex $x = #hp_hc_x$ to a #{hp_hc_n}-vertex Hamiltonian-path instance],
extra: [
#pred-commands(
"pred create --example " + problem-spec(hp_hc.source) + " -o hp.json",
"pred reduce hp.json --to " + target-spec(hp_hc) + " -o bundle.json",
"pred solve bundle.json",
"pred evaluate hp.json --config " + hp_hc_sol.source_config.map(str).join(","),
)

*Step 1 -- Inspect the source.* The canonical fixture has $n = #hp_hc_n$ vertices and $m = #hp_hc_m$ edges: #hp_hc_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). Its stored Hamiltonian path is $[#hp_hc_sol.source_config.map(str).join(", ")]$.

*Step 2 -- Add the universal vertex.* Set $x = n = #hp_hc_x$, copy the #hp_hc_m source edges, and add one edge from $x$ to each old vertex. The target therefore has $#hp_hc_target_n = #hp_hc_n + 1$ vertices and $#hp_hc_target_edges.len() = #hp_hc_m + #hp_hc_n$ edges: #hp_hc_target_edges.map(e => $(#e.at(0), #e.at(1))$).join(", ").

*Step 3 -- Close and verify the circuit.* Prefixing the source witness by $x$ gives the stored target witness $[#hp_hc_sol.target_config.map(str).join(", ")]$. The two circuit edges incident to $x$ are present because $x$ is universal; every edge between consecutive old vertices belongs to the source path.

*Step 4 -- Extract the path.* Rotate the target circuit to place $x$ first, then delete it. For the stored witness this returns $[#hp_hc_sol.target_config.slice(1).map(str).join(", ")]$, exactly the source Hamiltonian path.

*Multiplicity:* The fixture stores one canonical witness. On this $n >= 2$ branch, each ordered source-path witness gives $n + 1 = #hp_hc_target_n$ target configurations, namely the cyclic rotations of $[x]$ followed by that path; rotating a target witness to $x$ and deleting $x$ reverses this correspondence.
],
)[
The standard universal-vertex mapping @waggoner2025npcomplete is an $O(n + m)$ reduction. Given an undirected graph $G = (V, E)$ with $n = |V|$ and $m = |E|$, it adds one vertex adjacent to every old vertex when $n >= 2$; the implementation uses a fixed triangle when $n < 2$ so that the target model, which requires at least three vertices for a circuit, preserves the feasible empty and singleton path instances.
][
_Construction._ If $n >= 2$, introduce a fresh vertex $x = n$ and form $G' = (V', E')$ with
$V' = V union {x}$ and $E' = E union {{x, v} : v in V}$.
Hence the normal branch has exactly $|V'| = n + 1$ and $|E'| = m + n$. If $n < 2$, let $G'$ be the fixed triangle $K_3$. Across both branches, the registered safe bounds are $|V'| <= n + 3$ and $|E'| <= m + n + 3$.

_Correctness._ First suppose $n >= 2$. ($arrow.r.double$) If $(v_0, v_1, dots, v_(n-1))$ is a Hamiltonian path in $G$, then $(x, v_0, v_1, dots, v_(n-1))$ is a Hamiltonian circuit in $G'$: all internal edges come from the path, and the two closing edges incident to $x$ exist by construction. ($arrow.l.double$) If $G'$ has a Hamiltonian circuit, rotate its cyclic order until $x$ is first. Deleting $x$ leaves an ordering of every old vertex exactly once. Every consecutive pair in that ordering is joined by an edge of $E'$ not incident to $x$, hence by an original edge of $E$, so the ordering is a Hamiltonian path in $G$. If $n = 0$ or $n = 1$, the source configuration $()$ or $(0)$ is a valid Hamiltonian path under the model's permutation semantics, while the fixed triangle has a Hamiltonian circuit; thus equivalence also holds on the small-instance branch.

_Solution extraction._ For $n >= 2$, locate $x$ in the target permutation, cyclically rotate the circuit so that $x$ comes first, and delete $x$; preserve the remaining order as the source path. For $n = 0$ return $()$, and for $n = 1$ return $(0)$.

_Loops and parallel edges._ The graph representation permits self-loops and repeated edges. The construction copies them verbatim, so the exact normal-branch count remains $m + n$. They do not affect the proof: a Hamiltonian witness is a permutation of distinct vertices, and feasibility only asks whether each required adjacency is present.
]

#let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath")
#let hc_hp_sol = hc_hp.solutions.at(0)
#let hc_hp_n = graph-num-vertices(hc_hp.source.instance)
Expand Down
9 changes: 8 additions & 1 deletion docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2114,6 +2114,14 @@ @misc{mit6854MinCostFlow
url = {https://courses.csail.mit.edu/6.854/21/Scribe/s10-minCostFlowAlg/s10-minCostFlowAlg.html}
}

@misc{waggoner2025npcomplete,
author = {Bowen Waggoner},
title = {Standard 21: {P} and {NP} 2: {NP}-Completeness},
year = {2025},
howpublished = {CSCI 3104 course notes},
url = {https://bowaggoner.com/courses/2025/csci3104/book/standards/21-pnp-complete.html}
}

@inproceedings{chandran_et_al:LIPIcs.IPEC.2016.11,
author = {Chandran, Sunil and Issac, Davis and Karrenbauer, Andreas},
title = {{On the Parameterized Complexity of Biclique Cover and Partition}},
Expand Down Expand Up @@ -2142,4 +2150,3 @@ @article{berlekampMcElieceTilborg1978
year = {1978},
doi = {10.1109/TIT.1978.1055873}
}

100 changes: 100 additions & 0 deletions src/rules/hamiltonianpath_hamiltoniancircuit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Reduction from HamiltonianPath to HamiltonianCircuit.
//!
//! For a graph with at least two vertices, the construction copies the source
//! graph and adds one universal vertex. Deleting that vertex from any target
//! Hamiltonian circuit leaves a Hamiltonian path in the source graph. Empty and
//! singleton sources reduce to a fixed triangle because both are feasible in
//! the HamiltonianPath model.

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

/// Result of reducing HamiltonianPath to HamiltonianCircuit.
#[derive(Debug, Clone)]
pub struct ReductionHamiltonianPathToHamiltonianCircuit {
target: HamiltonianCircuit<SimpleGraph>,
num_original_vertices: usize,
}

impl ReductionResult for ReductionHamiltonianPathToHamiltonianCircuit {
type Source = HamiltonianPath<SimpleGraph>;
type Target = HamiltonianCircuit<SimpleGraph>;

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

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
let n = self.num_original_vertices;
if n < 2 {
return (0..n).collect();
}

let universal = n;
let universal_position = target_solution
.iter()
.position(|&vertex| vertex == universal)
.expect("target Hamiltonian circuit must contain the universal vertex");

target_solution[universal_position + 1..]
.iter()
.chain(&target_solution[..universal_position])
.copied()
.collect()
}
}

#[reduction(
overhead = {
num_vertices = "num_vertices + 3",
num_edges = "num_edges + num_vertices + 3",
}
)]
impl ReduceTo<HamiltonianCircuit<SimpleGraph>> for HamiltonianPath<SimpleGraph> {
type Result = ReductionHamiltonianPathToHamiltonianCircuit;

fn reduce_to(&self) -> Self::Result {
let n = self.num_vertices();
let target_graph = if n < 2 {
SimpleGraph::cycle(3)
} else {
let universal = n;
let mut edges = self.graph().edges();
edges.extend((0..n).map(|vertex| (universal, vertex)));
SimpleGraph::new(n + 1, edges)
};

ReductionHamiltonianPathToHamiltonianCircuit {
target: HamiltonianCircuit::new(target_graph),
num_original_vertices: n,
}
}
}

#[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: "hamiltonianpath_to_hamiltoniancircuit",
build: || {
let source = HamiltonianPath::new(SimpleGraph::new(
5,
vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 2), (1, 3)],
));
crate::example_db::specs::rule_example_with_witness::<_, HamiltonianCircuit<SimpleGraph>>(
source,
SolutionPair {
source_config: vec![0, 1, 2, 3, 4],
target_config: vec![5, 0, 1, 2, 3, 4],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/hamiltonianpath_hamiltoniancircuit.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 @@ -41,6 +41,7 @@ pub(crate) mod hamiltoniancircuit_stackercrane;
pub(crate) mod hamiltoniancircuit_strongconnectivityaugmentation;
pub(crate) mod hamiltoniancircuit_travelingsalesman;
pub(crate) mod hamiltonianpath_degreeconstrainedspanningtree;
pub(crate) mod hamiltonianpath_hamiltoniancircuit;
pub(crate) mod hamiltonianpath_isomorphicspanningtree;
pub(crate) mod hamiltonianpathbetweentwovertices_longestpath;
pub(crate) mod ilp_i32_ilp_bool;
Expand Down Expand Up @@ -452,6 +453,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(hamiltoniancircuit_strongconnectivityaugmentation::canonical_rule_example_specs());
specs.extend(hamiltoniancircuit_travelingsalesman::canonical_rule_example_specs());
specs.extend(hamiltonianpath_degreeconstrainedspanningtree::canonical_rule_example_specs());
specs.extend(hamiltonianpath_hamiltoniancircuit::canonical_rule_example_specs());
specs.extend(graphpartitioning_maxcut::canonical_rule_example_specs());
specs.extend(graphpartitioning_qubo::canonical_rule_example_specs());
specs.extend(hamiltonianpathbetweentwovertices_longestpath::canonical_rule_example_specs());
Expand Down
5 changes: 5 additions & 0 deletions src/unit_tests/rules/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ fn test_find_dominated_rules_returns_known_set() {
"PartitionIntoPathsOfLength2 {graph: \"SimpleGraph\"}",
"ILP {variable: \"bool\"}",
),
// HP → HC → RuralPostman → ILP is no worse than direct HP → ILP.
(
"HamiltonianPath {graph: \"SimpleGraph\"}",
"ILP {variable: \"bool\"}",
),
]
.into_iter()
.collect();
Expand Down
134 changes: 134 additions & 0 deletions src/unit_tests/rules/hamiltonianpath_hamiltoniancircuit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use crate::models::graph::{HamiltonianCircuit, HamiltonianPath};
use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target;
use crate::rules::{ReduceTo, ReductionResult};
use crate::solvers::BruteForce;
use crate::topology::{Graph, SimpleGraph};
use crate::Problem;

fn canonical_path_example() -> HamiltonianPath<SimpleGraph> {
HamiltonianPath::new(SimpleGraph::new(
5,
vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 2), (1, 3)],
))
}

#[test]
fn test_hamiltonianpath_to_hamiltoniancircuit_closed_loop() {
let source = canonical_path_example();
let reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&source);

assert_satisfaction_round_trip_from_satisfaction_target(
&source,
&reduction,
"HamiltonianPath -> HamiltonianCircuit",
);

let target = reduction.target_problem();
assert_eq!(target.num_vertices(), 6);
assert_eq!(target.num_edges(), 11);
assert_eq!(target.graph().neighbors(5).len(), 5);
for vertex in 0..5 {
assert!(target.graph().has_edge(5, vertex));
}
for edge in source.graph().edges() {
assert!(target.graph().has_edge(edge.0, edge.1));
}
}

#[test]
fn test_extracts_rotated_and_reversed_circuits() {
let source = canonical_path_example();
let reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&source);

let rotated = vec![2, 3, 4, 5, 0, 1];
assert!(reduction.target_problem().evaluate(&rotated).0);
assert_eq!(reduction.extract_solution(&rotated), vec![0, 1, 2, 3, 4]);

let reversed_and_rotated = vec![2, 1, 0, 5, 4, 3];
assert!(reduction.target_problem().evaluate(&reversed_and_rotated).0);
assert_eq!(
reduction.extract_solution(&reversed_and_rotated),
vec![4, 3, 2, 1, 0]
);
}

#[test]
fn test_empty_and_singleton_sources_use_feasible_triangle() {
let empty = HamiltonianPath::new(SimpleGraph::empty(0));
let empty_reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&empty);
assert_eq!(
empty_reduction.target_problem().graph(),
&SimpleGraph::cycle(3)
);
assert_eq!(
empty_reduction.extract_solution(&[0, 1, 2]),
Vec::<usize>::new()
);

let singleton = HamiltonianPath::new(SimpleGraph::empty(1));
let singleton_reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&singleton);
assert_eq!(
singleton_reduction.target_problem().graph(),
&SimpleGraph::cycle(3)
);
assert_eq!(singleton_reduction.extract_solution(&[2, 1, 0]), vec![0]);
}

#[test]
fn test_two_vertex_boundary_cases() {
let isolated = HamiltonianPath::new(SimpleGraph::empty(2));
let isolated_reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&isolated);
assert_eq!(isolated_reduction.target_problem().num_vertices(), 3);
assert_eq!(isolated_reduction.target_problem().num_edges(), 2);
assert!(BruteForce::new()
.find_witness(isolated_reduction.target_problem())
.is_none());

let one_edge = HamiltonianPath::new(SimpleGraph::new(2, vec![(0, 1)]));
let one_edge_reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&one_edge);
let target_solution = BruteForce::new()
.find_witness(one_edge_reduction.target_problem())
.expect("one source edge must extend to a target triangle");
let extracted = one_edge_reduction.extract_solution(&target_solution);
assert!(one_edge.evaluate(&extracted).0);
}

#[test]
fn test_star_and_disconnected_sources_remain_infeasible() {
let star = HamiltonianPath::new(SimpleGraph::star(5));
let star_reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&star);
assert!(BruteForce::new()
.find_witness(star_reduction.target_problem())
.is_none());

let disconnected = HamiltonianPath::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (3, 4)]));
let disconnected_reduction =
ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&disconnected);
assert!(BruteForce::new()
.find_witness(disconnected_reduction.target_problem())
.is_none());
}

#[test]
fn test_self_loops_and_parallel_edges_are_copied() {
let source = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 0), (0, 1), (0, 1), (1, 2)]));
let reduction = ReduceTo::<HamiltonianCircuit<SimpleGraph>>::reduce_to(&source);
let target = reduction.target_problem();

assert_eq!(target.num_vertices(), 4);
assert_eq!(target.num_edges(), 7);
assert_eq!(
target
.graph()
.edges()
.into_iter()
.filter(|&(u, v)| (u == 0 && v == 1) || (u == 1 && v == 0))
.count(),
2
);
assert!(target.graph().has_edge(0, 0));

let target_solution = vec![3, 0, 1, 2];
assert!(target.evaluate(&target_solution).0);
assert_eq!(reduction.extract_solution(&target_solution), vec![0, 1, 2]);
}