Skip to content

Commit fa0e238

Browse files
zazabapclaudeGiggleLiu
authored
Fix #217: Add HamiltonianPath model (#621)
* Add plan for #217: HamiltonianPath model * Implement #217: Add HamiltonianPath model * Review fixes: add resolve_alias entry and register example in integration tests * chore: remove plan file after implementation * Fix Copilot review comments on HamiltonianPath PR - Document configuration semantics (vertex ordering/permutation) in HamiltonianPath struct doc comment, including dims() encoding rationale - Add explicit `use crate::traits::Problem` import in test_hamiltonian_path_brute_force for consistency with other test functions - Add CLI create support for HamiltonianPath: example_for hint, create_problem match arm, create_random match arm, and updated error message Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add paper section and trait consistency for HamiltonianPath Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 291cf56 commit fa0e238

14 files changed

Lines changed: 408 additions & 7 deletions

File tree

docs/paper/reductions.typ

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"MinimumVertexCover": [Minimum Vertex Cover],
3131
"MaxCut": [Max-Cut],
3232
"GraphPartitioning": [Graph Partitioning],
33+
"HamiltonianPath": [Hamiltonian Path],
3334
"KColoring": [$k$-Coloring],
3435
"MinimumDominatingSet": [Minimum Dominating Set],
3536
"MaximumMatching": [Maximum Matching],
@@ -439,6 +440,15 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
439440
caption: [Graph with $n = 6$ vertices partitioned into $A = {v_0, v_1, v_2}$ (blue) and $B = {v_3, v_4, v_5}$ (red). The 3 crossing edges $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$ are shown in bold red; internal edges are gray.],
440441
) <fig:graph-partitioning>
441442
]
443+
#problem-def("HamiltonianPath")[
444+
Given a graph $G = (V, E)$, determine whether $G$ contains a _Hamiltonian path_, i.e., a simple path that visits every vertex exactly once.
445+
][
446+
A classical NP-complete decision problem from Garey & Johnson (A1.3 GT39), closely related to _Hamiltonian Circuit_. Finding a Hamiltonian path in $G$ is equivalent to finding a Hamiltonian circuit in an augmented graph $G'$ obtained by adding a new vertex adjacent to all vertices of $G$. The problem remains NP-complete for planar graphs, cubic graphs, and bipartite graphs.
447+
448+
The best known exact algorithm is Björklund's randomized $O^*(1.657^n)$ "Determinant Sums" method @bjorklund2014, which applies to both Hamiltonian path and circuit. The classical Held--Karp dynamic programming algorithm solves it in $O(n^2 dot 2^n)$ deterministic time.
449+
450+
Variables: $n = |V|$ values forming a permutation. Position $i$ holds the vertex visited at step $i$. A configuration is satisfying when it forms a valid permutation of all vertices and consecutive vertices are adjacent in $G$.
451+
]
442452
#problem-def("KColoring")[
443453
Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$.
444454
][

docs/paper/references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ @article{bjorklund2009
196196
doi = {10.1137/070683933}
197197
}
198198

199+
@article{bjorklund2014,
200+
author = {Andreas Bj{\"o}rklund},
201+
title = {Determinant Sums for Undirected Hamiltonicity},
202+
journal = {SIAM Journal on Computing},
203+
volume = {43},
204+
number = {1},
205+
pages = {280--299},
206+
year = {2014},
207+
doi = {10.1137/110839229},
208+
}
209+
199210
@article{aspvall1979,
200211
author = {Bengt Aspvall and Michael F. Plass and Robert Endre Tarjan},
201212
title = {A Linear-Time Algorithm for Testing the Truth of Certain Quantified Boolean Formulas},

docs/src/reductions/problem_schemas.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@
121121
}
122122
]
123123
},
124+
{
125+
"name": "HamiltonianPath",
126+
"description": "Find a Hamiltonian path in a graph",
127+
"fields": [
128+
{
129+
"name": "graph",
130+
"type_name": "G",
131+
"description": "The underlying graph G=(V,E)"
132+
}
133+
]
134+
},
124135
{
125136
"name": "ILP",
126137
"description": "Optimize linear objective subject to linear constraints",

examples/hamiltonian_path.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use problemreductions::models::graph::HamiltonianPath;
2+
use problemreductions::topology::SimpleGraph;
3+
use problemreductions::{BruteForce, Problem};
4+
5+
pub fn run() {
6+
// Instance 2 from issue: 6 vertices, 8 edges (non-trivial)
7+
let graph = SimpleGraph::new(
8+
6,
9+
vec![
10+
(0, 1),
11+
(0, 2),
12+
(1, 3),
13+
(2, 3),
14+
(3, 4),
15+
(3, 5),
16+
(4, 2),
17+
(5, 1),
18+
],
19+
);
20+
let problem = HamiltonianPath::new(graph);
21+
22+
println!("HamiltonianPath instance:");
23+
println!(" Vertices: {}", problem.num_vertices());
24+
println!(" Edges: {}", problem.num_edges());
25+
26+
let json = serde_json::to_string_pretty(&problem).unwrap();
27+
println!(" JSON: {}", json);
28+
29+
// Find all Hamiltonian paths
30+
let solver = BruteForce::new();
31+
let solutions = solver.find_all_satisfying(&problem);
32+
println!(" Solutions found: {}", solutions.len());
33+
34+
for (i, sol) in solutions.iter().enumerate() {
35+
println!(" Path {}: {:?} (valid: {})", i, sol, problem.evaluate(sol));
36+
}
37+
}
38+
39+
fn main() {
40+
run();
41+
}

problemreductions-cli/src/commands/create.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant};
55
use crate::util;
66
use anyhow::{bail, Context, Result};
77
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
8-
use problemreductions::models::graph::GraphPartitioning;
8+
use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath};
99
use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum};
1010
use problemreductions::prelude::*;
1111
use problemreductions::registry::collect_schemas;
@@ -85,6 +85,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
8585
_ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1",
8686
},
8787
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
88+
"HamiltonianPath" => "--graph 0-1,1-2,2-3",
8889
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
8990
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
9091
}
@@ -228,6 +229,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
228229
)
229230
}
230231

232+
// Hamiltonian path (graph only, no weights)
233+
"HamiltonianPath" => {
234+
let (graph, _) = parse_graph(args).map_err(|e| {
235+
anyhow::anyhow!(
236+
"{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3"
237+
)
238+
})?;
239+
(
240+
ser(HamiltonianPath::new(graph))?,
241+
resolved_variant.clone(),
242+
)
243+
}
244+
231245
// Graph problems with edge weights
232246
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
233247
let (graph, _) = parse_graph(args).map_err(|e| {
@@ -1204,6 +1218,17 @@ fn create_random(
12041218
(ser(GraphPartitioning::new(graph))?, variant)
12051219
}
12061220

1221+
// HamiltonianPath (graph only, no weights)
1222+
"HamiltonianPath" => {
1223+
let edge_prob = args.edge_prob.unwrap_or(0.5);
1224+
if !(0.0..=1.0).contains(&edge_prob) {
1225+
bail!("--edge-prob must be between 0.0 and 1.0");
1226+
}
1227+
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
1228+
let variant = variant_map(&[("graph", "SimpleGraph")]);
1229+
(ser(HamiltonianPath::new(graph))?, variant)
1230+
}
1231+
12071232
// Graph problems with edge weights
12081233
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
12091234
let edge_prob = args.edge_prob.unwrap_or(0.5);
@@ -1255,7 +1280,8 @@ fn create_random(
12551280
_ => bail!(
12561281
"Random generation is not supported for {canonical}. \
12571282
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
1258-
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman)"
1283+
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
1284+
HamiltonianPath)"
12591285
),
12601286
};
12611287

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ pub fn load_problem(
212212
"MinimumDominatingSet" => deser_opt::<MinimumDominatingSet<SimpleGraph, i32>>(data),
213213
"MinimumSumMulticenter" => deser_opt::<MinimumSumMulticenter<SimpleGraph, i32>>(data),
214214
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
215+
"HamiltonianPath" => deser_sat::<HamiltonianPath<SimpleGraph>>(data),
215216
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
216217
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
217218
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
@@ -278,6 +279,7 @@ pub fn serialize_any_problem(
278279
"MinimumDominatingSet" => try_ser::<MinimumDominatingSet<SimpleGraph, i32>>(any),
279280
"MinimumSumMulticenter" => try_ser::<MinimumSumMulticenter<SimpleGraph, i32>>(any),
280281
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
282+
"HamiltonianPath" => try_ser::<HamiltonianPath<SimpleGraph>>(any),
281283
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
282284
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
283285
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),

problemreductions-cli/src/problem_name.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub fn resolve_alias(input: &str) -> String {
6666
"fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(),
6767
"minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(),
6868
"subsetsum" => "SubsetSum".to_string(),
69+
"hamiltonianpath" => "HamiltonianPath".to_string(),
6970
_ => input.to_string(), // pass-through for exact names
7071
}
7172
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub mod prelude {
4141
pub use crate::models::algebraic::{BMF, QUBO};
4242
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
4343
pub use crate::models::graph::{
44-
BicliqueCover, GraphPartitioning, SpinGlass, SubgraphIsomorphism,
44+
BicliqueCover, GraphPartitioning, HamiltonianPath, SpinGlass, SubgraphIsomorphism,
4545
};
4646
pub use crate::models::graph::{
4747
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//! Hamiltonian Path problem implementation.
2+
//!
3+
//! The Hamiltonian Path problem asks whether a graph contains a simple path
4+
//! that visits every vertex exactly once.
5+
6+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
7+
use crate::topology::{Graph, SimpleGraph};
8+
use crate::traits::{Problem, SatisfactionProblem};
9+
use crate::variant::VariantParam;
10+
use serde::{Deserialize, Serialize};
11+
12+
inventory::submit! {
13+
ProblemSchemaEntry {
14+
name: "HamiltonianPath",
15+
module_path: module_path!(),
16+
description: "Find a Hamiltonian path in a graph",
17+
fields: &[
18+
FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
19+
],
20+
}
21+
}
22+
23+
/// The Hamiltonian Path problem.
24+
///
25+
/// Given a graph G = (V, E), determine whether G contains a Hamiltonian path,
26+
/// i.e., a simple path that visits every vertex exactly once.
27+
///
28+
/// # Representation
29+
///
30+
/// A configuration is a sequence of `n` vertex indices representing a vertex
31+
/// ordering (permutation). Each position `i` in the configuration holds the
32+
/// vertex visited at step `i`. A valid solution must be a permutation of
33+
/// `0..n` where consecutive entries are adjacent in the graph.
34+
///
35+
/// The search space has `dims() = [n; n]` (each position can hold any of `n`
36+
/// vertices), so brute-force enumerates `n^n` configurations. Only `n!`
37+
/// permutations can satisfy the constraints, but the encoding avoids complex
38+
/// variable-domain schemes and matches the problem's natural formulation.
39+
///
40+
/// # Type Parameters
41+
///
42+
/// * `G` - Graph type (e.g., SimpleGraph)
43+
///
44+
/// # Example
45+
///
46+
/// ```
47+
/// use problemreductions::models::graph::HamiltonianPath;
48+
/// use problemreductions::topology::SimpleGraph;
49+
/// use problemreductions::{Problem, Solver, BruteForce};
50+
///
51+
/// // Path graph: 0-1-2-3
52+
/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]);
53+
/// let problem = HamiltonianPath::new(graph);
54+
///
55+
/// let solver = BruteForce::new();
56+
/// let solution = solver.find_satisfying(&problem);
57+
/// assert!(solution.is_some());
58+
/// ```
59+
#[derive(Debug, Clone, Serialize, Deserialize)]
60+
#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
61+
pub struct HamiltonianPath<G> {
62+
graph: G,
63+
}
64+
65+
impl<G: Graph> HamiltonianPath<G> {
66+
/// Create a new Hamiltonian Path problem from a graph.
67+
pub fn new(graph: G) -> Self {
68+
Self { graph }
69+
}
70+
71+
/// Get a reference to the underlying graph.
72+
pub fn graph(&self) -> &G {
73+
&self.graph
74+
}
75+
76+
/// Get the number of vertices in the underlying graph.
77+
pub fn num_vertices(&self) -> usize {
78+
self.graph.num_vertices()
79+
}
80+
81+
/// Get the number of edges in the underlying graph.
82+
pub fn num_edges(&self) -> usize {
83+
self.graph.num_edges()
84+
}
85+
86+
/// Check if a configuration is a valid Hamiltonian path.
87+
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
88+
is_valid_hamiltonian_path(&self.graph, config)
89+
}
90+
}
91+
92+
impl<G> Problem for HamiltonianPath<G>
93+
where
94+
G: Graph + VariantParam,
95+
{
96+
const NAME: &'static str = "HamiltonianPath";
97+
type Metric = bool;
98+
99+
fn variant() -> Vec<(&'static str, &'static str)> {
100+
crate::variant_params![G]
101+
}
102+
103+
fn dims(&self) -> Vec<usize> {
104+
let n = self.graph.num_vertices();
105+
vec![n; n]
106+
}
107+
108+
fn evaluate(&self, config: &[usize]) -> bool {
109+
is_valid_hamiltonian_path(&self.graph, config)
110+
}
111+
}
112+
113+
impl<G: Graph + VariantParam> SatisfactionProblem for HamiltonianPath<G> {}
114+
115+
/// Check if a configuration represents a valid Hamiltonian path in the graph.
116+
///
117+
/// A valid Hamiltonian path is a permutation of the vertices such that
118+
/// consecutive vertices in the permutation are adjacent in the graph.
119+
pub(crate) fn is_valid_hamiltonian_path<G: Graph>(graph: &G, config: &[usize]) -> bool {
120+
let n = graph.num_vertices();
121+
if config.len() != n {
122+
return false;
123+
}
124+
125+
// Check that config is a valid permutation of 0..n
126+
let mut seen = vec![false; n];
127+
for &v in config {
128+
if v >= n || seen[v] {
129+
return false;
130+
}
131+
seen[v] = true;
132+
}
133+
134+
// Check consecutive vertices are adjacent
135+
for i in 0..n.saturating_sub(1) {
136+
if !graph.has_edge(config[i], config[i + 1]) {
137+
return false;
138+
}
139+
}
140+
141+
true
142+
}
143+
144+
// Use Bjorklund (2014) O*(1.657^n) as best known for general undirected graphs
145+
crate::declare_variants! {
146+
HamiltonianPath<SimpleGraph> => "1.657^num_vertices",
147+
}
148+
149+
#[cfg(test)]
150+
#[path = "../../unit_tests/models/graph/hamiltonian_path.rs"]
151+
mod tests;

src/models/graph/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
//! - [`MaximumMatching`]: Maximum weight matching
1515
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
1616
//! - [`SpinGlass`]: Ising model Hamiltonian
17+
//! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex)
1718
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
1819
//! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs
1920
//! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median)
@@ -22,6 +23,7 @@
2223
2324
pub(crate) mod biclique_cover;
2425
pub(crate) mod graph_partitioning;
26+
pub(crate) mod hamiltonian_path;
2527
pub(crate) mod kcoloring;
2628
pub(crate) mod max_cut;
2729
pub(crate) mod maximal_is;
@@ -41,6 +43,7 @@ pub(crate) mod traveling_salesman;
4143

4244
pub use biclique_cover::BicliqueCover;
4345
pub use graph_partitioning::GraphPartitioning;
46+
pub use hamiltonian_path::HamiltonianPath;
4447
pub use kcoloring::KColoring;
4548
pub use max_cut::MaxCut;
4649
pub use maximal_is::MaximalIS;

0 commit comments

Comments
 (0)