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
10 changes: 10 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"MinimumVertexCover": [Minimum Vertex Cover],
"MaxCut": [Max-Cut],
"GraphPartitioning": [Graph Partitioning],
"HamiltonianPath": [Hamiltonian Path],
"KColoring": [$k$-Coloring],
"MinimumDominatingSet": [Minimum Dominating Set],
"MaximumMatching": [Maximum Matching],
Expand Down Expand Up @@ -439,6 +440,15 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
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.],
) <fig:graph-partitioning>
]
#problem-def("HamiltonianPath")[
Given a graph $G = (V, E)$, determine whether $G$ contains a _Hamiltonian path_, i.e., a simple path that visits every vertex exactly once.
][
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.

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.

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$.
]
#problem-def("KColoring")[
Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$.
][
Expand Down
11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ @article{bjorklund2009
doi = {10.1137/070683933}
}

@article{bjorklund2014,
author = {Andreas Bj{\"o}rklund},
title = {Determinant Sums for Undirected Hamiltonicity},
journal = {SIAM Journal on Computing},
volume = {43},
number = {1},
pages = {280--299},
year = {2014},
doi = {10.1137/110839229},
}

@article{aspvall1979,
author = {Bengt Aspvall and Michael F. Plass and Robert Endre Tarjan},
title = {A Linear-Time Algorithm for Testing the Truth of Certain Quantified Boolean Formulas},
Expand Down
11 changes: 11 additions & 0 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@
}
]
},
{
"name": "HamiltonianPath",
"description": "Find a Hamiltonian path in a graph",
"fields": [
{
"name": "graph",
"type_name": "G",
"description": "The underlying graph G=(V,E)"
}
]
},
{
"name": "ILP",
"description": "Optimize linear objective subject to linear constraints",
Expand Down
41 changes: 41 additions & 0 deletions examples/hamiltonian_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use problemreductions::models::graph::HamiltonianPath;
use problemreductions::topology::SimpleGraph;
use problemreductions::{BruteForce, Problem};

pub fn run() {
// Instance 2 from issue: 6 vertices, 8 edges (non-trivial)
let graph = SimpleGraph::new(
6,
vec![
(0, 1),
(0, 2),
(1, 3),
(2, 3),
(3, 4),
(3, 5),
(4, 2),
(5, 1),
],
);
let problem = HamiltonianPath::new(graph);

println!("HamiltonianPath instance:");
println!(" Vertices: {}", problem.num_vertices());
println!(" Edges: {}", problem.num_edges());

let json = serde_json::to_string_pretty(&problem).unwrap();
println!(" JSON: {}", json);

// Find all Hamiltonian paths
let solver = BruteForce::new();
let solutions = solver.find_all_satisfying(&problem);
println!(" Solutions found: {}", solutions.len());

for (i, sol) in solutions.iter().enumerate() {
println!(" Path {}: {:?} (valid: {})", i, sol, problem.evaluate(sol));
}
}

fn main() {
run();
}
30 changes: 28 additions & 2 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant};
use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
use problemreductions::models::graph::GraphPartitioning;
use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath};
use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum};
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
Expand Down Expand Up @@ -85,6 +85,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
_ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1",
},
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
"HamiltonianPath" => "--graph 0-1,1-2,2-3",
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
}
Expand Down Expand Up @@ -228,6 +229,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// Hamiltonian path (graph only, no weights)
"HamiltonianPath" => {
let (graph, _) = parse_graph(args).map_err(|e| {
anyhow::anyhow!(
"{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3"
)
})?;
(
ser(HamiltonianPath::new(graph))?,
resolved_variant.clone(),
)
}

// Graph problems with edge weights
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
let (graph, _) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -1204,6 +1218,17 @@ fn create_random(
(ser(GraphPartitioning::new(graph))?, variant)
}

// HamiltonianPath (graph only, no weights)
"HamiltonianPath" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
bail!("--edge-prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
let variant = variant_map(&[("graph", "SimpleGraph")]);
(ser(HamiltonianPath::new(graph))?, variant)
}

// Graph problems with edge weights
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
Expand Down Expand Up @@ -1255,7 +1280,8 @@ fn create_random(
_ => bail!(
"Random generation is not supported for {canonical}. \
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman)"
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
HamiltonianPath)"
),
};

Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ pub fn load_problem(
"MinimumDominatingSet" => deser_opt::<MinimumDominatingSet<SimpleGraph, i32>>(data),
"MinimumSumMulticenter" => deser_opt::<MinimumSumMulticenter<SimpleGraph, i32>>(data),
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
"HamiltonianPath" => deser_sat::<HamiltonianPath<SimpleGraph>>(data),
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
Expand Down Expand Up @@ -278,6 +279,7 @@ pub fn serialize_any_problem(
"MinimumDominatingSet" => try_ser::<MinimumDominatingSet<SimpleGraph, i32>>(any),
"MinimumSumMulticenter" => try_ser::<MinimumSumMulticenter<SimpleGraph, i32>>(any),
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
"HamiltonianPath" => try_ser::<HamiltonianPath<SimpleGraph>>(any),
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub fn resolve_alias(input: &str) -> String {
"fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(),
"minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(),
"subsetsum" => "SubsetSum".to_string(),
"hamiltonianpath" => "HamiltonianPath".to_string(),
_ => input.to_string(), // pass-through for exact names
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub mod prelude {
pub use crate::models::algebraic::{BMF, QUBO};
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
pub use crate::models::graph::{
BicliqueCover, GraphPartitioning, SpinGlass, SubgraphIsomorphism,
BicliqueCover, GraphPartitioning, HamiltonianPath, SpinGlass, SubgraphIsomorphism,
};
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
Expand Down
151 changes: 151 additions & 0 deletions src/models/graph/hamiltonian_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//! Hamiltonian Path problem implementation.
//!
//! The Hamiltonian Path problem asks whether a graph contains a simple path
//! that visits every vertex exactly once.

use crate::registry::{FieldInfo, ProblemSchemaEntry};
use crate::topology::{Graph, SimpleGraph};
use crate::traits::{Problem, SatisfactionProblem};
use crate::variant::VariantParam;
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "HamiltonianPath",
module_path: module_path!(),
description: "Find a Hamiltonian path in a graph",
fields: &[
FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
],
}
}

/// The Hamiltonian Path problem.
///
/// Given a graph G = (V, E), determine whether G contains a Hamiltonian path,
/// i.e., a simple path that visits every vertex exactly once.
///
/// # Representation
///
/// A configuration is a sequence of `n` vertex indices representing a vertex
/// ordering (permutation). Each position `i` in the configuration holds the
/// vertex visited at step `i`. A valid solution must be a permutation of
/// `0..n` where consecutive entries are adjacent in the graph.
///
/// The search space has `dims() = [n; n]` (each position can hold any of `n`
/// vertices), so brute-force enumerates `n^n` configurations. Only `n!`
/// permutations can satisfy the constraints, but the encoding avoids complex
/// variable-domain schemes and matches the problem's natural formulation.
///
/// # Type Parameters
///
/// * `G` - Graph type (e.g., SimpleGraph)
///
/// # Example
///
/// ```
/// use problemreductions::models::graph::HamiltonianPath;
/// use problemreductions::topology::SimpleGraph;
/// use problemreductions::{Problem, Solver, BruteForce};
///
/// // Path graph: 0-1-2-3
/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]);
/// let problem = HamiltonianPath::new(graph);
///
/// let solver = BruteForce::new();
/// let solution = solver.find_satisfying(&problem);
/// assert!(solution.is_some());
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
pub struct HamiltonianPath<G> {
graph: G,
}

impl<G: Graph> HamiltonianPath<G> {
/// Create a new Hamiltonian Path problem from a graph.
pub fn new(graph: G) -> Self {
Self { graph }
}

/// Get a reference to the underlying graph.
pub fn graph(&self) -> &G {
&self.graph
}

/// Get the number of vertices in the underlying graph.
pub fn num_vertices(&self) -> usize {
self.graph.num_vertices()
}

/// Get the number of edges in the underlying graph.
pub fn num_edges(&self) -> usize {
self.graph.num_edges()
}

/// Check if a configuration is a valid Hamiltonian path.
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
is_valid_hamiltonian_path(&self.graph, config)
}
}

impl<G> Problem for HamiltonianPath<G>
where
G: Graph + VariantParam,
{
const NAME: &'static str = "HamiltonianPath";
type Metric = bool;

fn variant() -> Vec<(&'static str, &'static str)> {
crate::variant_params![G]
}

fn dims(&self) -> Vec<usize> {
let n = self.graph.num_vertices();
vec![n; n]
}
Comment on lines +103 to +106

fn evaluate(&self, config: &[usize]) -> bool {
is_valid_hamiltonian_path(&self.graph, config)
}
}

impl<G: Graph + VariantParam> SatisfactionProblem for HamiltonianPath<G> {}

/// Check if a configuration represents a valid Hamiltonian path in the graph.
///
/// A valid Hamiltonian path is a permutation of the vertices such that
/// consecutive vertices in the permutation are adjacent in the graph.
pub(crate) fn is_valid_hamiltonian_path<G: Graph>(graph: &G, config: &[usize]) -> bool {
let n = graph.num_vertices();
if config.len() != n {
return false;
}

// Check that config is a valid permutation of 0..n
let mut seen = vec![false; n];
for &v in config {
if v >= n || seen[v] {
return false;
}
seen[v] = true;
}

// Check consecutive vertices are adjacent
for i in 0..n.saturating_sub(1) {
if !graph.has_edge(config[i], config[i + 1]) {
return false;
}
}

true
}

// Use Bjorklund (2014) O*(1.657^n) as best known for general undirected graphs
crate::declare_variants! {
HamiltonianPath<SimpleGraph> => "1.657^num_vertices",
}

#[cfg(test)]
#[path = "../../unit_tests/models/graph/hamiltonian_path.rs"]
mod tests;
3 changes: 3 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! - [`MaximumMatching`]: Maximum weight matching
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
//! - [`SpinGlass`]: Ising model Hamiltonian
//! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex)
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
//! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs
//! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median)
Expand All @@ -22,6 +23,7 @@

pub(crate) mod biclique_cover;
pub(crate) mod graph_partitioning;
pub(crate) mod hamiltonian_path;
pub(crate) mod kcoloring;
pub(crate) mod max_cut;
pub(crate) mod maximal_is;
Expand All @@ -41,6 +43,7 @@ pub(crate) mod traveling_salesman;

pub use biclique_cover::BicliqueCover;
pub use graph_partitioning::GraphPartitioning;
pub use hamiltonian_path::HamiltonianPath;
pub use kcoloring::KColoring;
pub use max_cut::MaxCut;
pub use maximal_is::MaximalIS;
Expand Down
7 changes: 3 additions & 4 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ pub mod set;
pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO};
pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
pub use graph::{
BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique,
BicliqueCover, GraphPartitioning, HamiltonianPath, KColoring, MaxCut, MaximalIS, MaximumClique,
MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet,
MinimumSumMulticenter,
MinimumFeedbackVertexSet, MinimumVertexCover, PartitionIntoTriangles, RuralPostman, SpinGlass,
SubgraphIsomorphism, TravelingSalesman,
MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles,
RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman,
};
pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum};
pub use set::{MaximumSetPacking, MinimumSetCovering};
Loading
Loading