diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 88cd07556..194d583f8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 186f70de2..d4cef86cc 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -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}}, @@ -2142,4 +2150,3 @@ @article{berlekampMcElieceTilborg1978 year = {1978}, doi = {10.1109/TIT.1978.1055873} } - diff --git a/src/rules/hamiltonianpath_hamiltoniancircuit.rs b/src/rules/hamiltonianpath_hamiltoniancircuit.rs new file mode 100644 index 000000000..339b2e663 --- /dev/null +++ b/src/rules/hamiltonianpath_hamiltoniancircuit.rs @@ -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, + num_original_vertices: usize, +} + +impl ReductionResult for ReductionHamiltonianPathToHamiltonianCircuit { + type Source = HamiltonianPath; + type Target = HamiltonianCircuit; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + 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> for HamiltonianPath { + 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 { + 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>( + 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; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 95eb5f477..edf7b83cd 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -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; @@ -452,6 +453,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec HamiltonianPath { + 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::>::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::>::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::>::reduce_to(&empty); + assert_eq!( + empty_reduction.target_problem().graph(), + &SimpleGraph::cycle(3) + ); + assert_eq!( + empty_reduction.extract_solution(&[0, 1, 2]), + Vec::::new() + ); + + let singleton = HamiltonianPath::new(SimpleGraph::empty(1)); + let singleton_reduction = ReduceTo::>::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::>::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::>::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::>::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::>::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::>::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]); +}