diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2b632fad..cf19345f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -47,6 +47,7 @@ "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], "GraphPartitioning": [Graph Partitioning], + "HamiltonianCircuit": [Hamiltonian Circuit], "HamiltonianPath": [Hamiltonian Path], "IsomorphicSpanningTree": [Isomorphic Spanning Tree], "KColoring": [$k$-Coloring], @@ -619,6 +620,21 @@ One of the most intensely studied NP-hard problems, with applications in logisti caption: [Complete graph $K_4$ with weighted edges. The optimal tour $v_0 -> v_1 -> v_2 -> v_3 -> v_0$ (blue edges) has cost 6.], ) ] +#problem-def("HamiltonianCircuit")[ + *Instance:* An undirected graph $G = (V, E)$. + + *Question:* Does $G$ contain a _Hamiltonian circuit_ --- a closed path that visits every vertex exactly once? +][ + The Hamiltonian Circuit problem is one of Karp's original 21 NP-complete problems @karp1972, and is listed as GT37 in Garey & Johnson @garey1979. + It is closely related to the Traveling Salesman Problem: while TSP seeks to minimize the total weight of a Hamiltonian cycle on a weighted complete graph, the Hamiltonian Circuit problem simply asks whether _any_ such cycle exists on a general (unweighted) graph. + + A configuration is a permutation $pi$ of the vertices, interpreted as the order in which they are visited. + The circuit is valid when every consecutive pair $(pi(i), pi(i+1 mod n))$ is an edge in $G$. + + *Algorithms.* + The classical Held--Karp dynamic programming algorithm @heldkarp1962 solves the problem in $O(n^2 dot 2^n)$ time and $O(n dot 2^n)$ space. + Björklund's randomized "Determinant Sums" algorithm achieves $O^*(1.657^n)$ time for general graphs and $O^*(sqrt(2)^n)$ for bipartite graphs @bjorklund2014. +] #problem-def("OptimalLinearArrangement")[ Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 64bd0b76..4bcb6f5c 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -237,6 +237,17 @@ @inproceedings{zamir2021 doi = {10.4230/LIPIcs.ICALP.2021.113} } +@article{bjorklund2014, + author = {Andreas Bj\"{o}rklund}, + title = {Determinant Sums for Undirected {H}amiltonicity}, + journal = {SIAM Journal on Computing}, + volume = {43}, + number = {1}, + pages = {280--299}, + year = {2014}, + doi = {10.1137/110839229} +} + @article{bjorklund2009, author = {Andreas Bj\"{o}rklund and Thore Husfeldt and Mikko Koivisto}, title = {Set Partitioning via Inclusion-Exclusion}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 43b2a445..cf327157 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -142,6 +142,17 @@ } ] }, + { + "name": "HamiltonianCircuit", + "description": "Does the graph contain a Hamiltonian circuit?", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The undirected graph G=(V,E)" + } + ] + }, { "name": "HamiltonianPath", "description": "Find a Hamiltonian path in a graph", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index bb8c0255..7d14c481 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -80,6 +80,15 @@ "doc_path": "models/graph/struct.GraphPartitioning.html", "complexity": "2^num_vertices" }, + { + "name": "HamiltonianCircuit", + "variant": { + "graph": "SimpleGraph" + }, + "category": "graph", + "doc_path": "models/graph/struct.HamiltonianCircuit.html", + "complexity": "1.657^num_vertices" + }, { "name": "HamiltonianPath", "variant": { @@ -505,7 +514,7 @@ "edges": [ { "source": 3, - "target": 11, + "target": 12, "overhead": [ { "field": "num_vars", @@ -520,7 +529,7 @@ }, { "source": 4, - "target": 11, + "target": 12, "overhead": [ { "field": "num_vars", @@ -535,7 +544,7 @@ }, { "source": 4, - "target": 52, + "target": 53, "overhead": [ { "field": "num_spins", @@ -565,7 +574,7 @@ }, { "source": 7, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -579,8 +588,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 11, - "target": 12, + "source": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -594,8 +603,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 11, - "target": 47, + "source": 12, + "target": 48, "overhead": [ { "field": "num_vars", @@ -605,8 +614,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 15, - "target": 18, + "source": 16, + "target": 19, "overhead": [ { "field": "num_vertices", @@ -620,8 +629,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 18, - "target": 11, + "source": 19, + "target": 12, "overhead": [ { "field": "num_vars", @@ -635,8 +644,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 18, - "target": 47, + "source": 19, + "target": 48, "overhead": [ { "field": "num_vars", @@ -646,8 +655,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 19, - "target": 21, + "source": 20, + "target": 22, "overhead": [ { "field": "num_vars", @@ -661,8 +670,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 19, - "target": 47, + "source": 20, + "target": 48, "overhead": [ { "field": "num_vars", @@ -672,8 +681,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 20, - "target": 21, + "source": 21, + "target": 22, "overhead": [ { "field": "num_vars", @@ -687,8 +696,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 47, + "source": 21, + "target": 48, "overhead": [ { "field": "num_vars", @@ -698,8 +707,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 20, - "target": 54, + "source": 21, + "target": 55, "overhead": [ { "field": "num_elements", @@ -709,8 +718,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 21, - "target": 49, + "source": 22, + "target": 50, "overhead": [ { "field": "num_clauses", @@ -728,8 +737,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 22, - "target": 47, + "source": 23, + "target": 48, "overhead": [ { "field": "num_vars", @@ -739,8 +748,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 23, - "target": 11, + "source": 24, + "target": 12, "overhead": [ { "field": "num_vars", @@ -754,8 +763,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 24, - "target": 52, + "source": 25, + "target": 53, "overhead": [ { "field": "num_spins", @@ -769,8 +778,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 26, - "target": 11, + "source": 27, + "target": 12, "overhead": [ { "field": "num_vars", @@ -784,8 +793,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 26, - "target": 30, + "source": 27, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -799,8 +808,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 27, - "target": 28, + "source": 28, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -814,8 +823,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 27, - "target": 32, + "source": 28, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -829,8 +838,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -844,8 +853,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 27, + "source": 30, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -859,8 +868,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 29, - "target": 30, + "source": 30, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -874,8 +883,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 31, + "source": 30, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -889,8 +898,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 29, - "target": 35, + "source": 30, + "target": 36, "overhead": [ { "field": "num_sets", @@ -904,8 +913,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 30, - "target": 26, + "source": 31, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -919,8 +928,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 30, - "target": 37, + "source": 31, + "target": 38, "overhead": [ { "field": "num_sets", @@ -934,8 +943,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 30, - "target": 43, + "source": 31, + "target": 44, "overhead": [ { "field": "num_vertices", @@ -949,8 +958,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 31, - "target": 33, + "source": 32, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -964,8 +973,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 32, - "target": 29, + "source": 33, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -979,8 +988,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 32, - "target": 33, + "source": 33, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -994,8 +1003,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1009,8 +1018,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 11, + "source": 35, + "target": 12, "overhead": [ { "field": "num_vars", @@ -1024,8 +1033,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 34, - "target": 37, + "source": 35, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1039,8 +1048,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 35, - "target": 29, + "source": 36, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -1054,8 +1063,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 35, - "target": 37, + "source": 36, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1069,8 +1078,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 36, - "target": 47, + "source": 37, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1080,8 +1089,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 37, - "target": 11, + "source": 38, + "target": 12, "overhead": [ { "field": "num_vars", @@ -1095,8 +1104,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 37, - "target": 30, + "source": 38, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1110,8 +1119,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 37, - "target": 36, + "source": 38, + "target": 37, "overhead": [ { "field": "num_sets", @@ -1125,8 +1134,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 38, - "target": 11, + "source": 39, + "target": 12, "overhead": [ { "field": "num_vars", @@ -1140,8 +1149,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 41, - "target": 11, + "source": 42, + "target": 12, "overhead": [ { "field": "num_vars", @@ -1155,8 +1164,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 43, - "target": 30, + "source": 44, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1170,8 +1179,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 43, - "target": 41, + "source": 44, + "target": 42, "overhead": [ { "field": "num_sets", @@ -1185,8 +1194,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 47, - "target": 11, + "source": 48, + "target": 12, "overhead": [ { "field": "num_vars", @@ -1200,8 +1209,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 47, - "target": 51, + "source": 48, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1211,7 +1220,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 49, + "source": 50, "target": 4, "overhead": [ { @@ -1226,8 +1235,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 49, - "target": 15, + "source": 50, + "target": 16, "overhead": [ { "field": "num_vertices", @@ -1241,8 +1250,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 49, - "target": 20, + "source": 50, + "target": 21, "overhead": [ { "field": "num_clauses", @@ -1256,8 +1265,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 49, - "target": 29, + "source": 50, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -1271,8 +1280,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 49, - "target": 38, + "source": 50, + "target": 39, "overhead": [ { "field": "num_vertices", @@ -1286,8 +1295,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 51, - "target": 47, + "source": 52, + "target": 48, "overhead": [ { "field": "num_vars", @@ -1297,8 +1306,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, - "target": 24, + "source": 53, + "target": 25, "overhead": [ { "field": "num_vertices", @@ -1312,8 +1321,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 52, - "target": 51, + "source": 53, + "target": 52, "overhead": [ { "field": "num_spins", @@ -1327,8 +1336,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 55, - "target": 11, + "source": 56, + "target": 12, "overhead": [ { "field": "num_vars", @@ -1342,8 +1351,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 55, - "target": 47, + "source": 56, + "target": 48, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 0e67c051..434d26c5 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -223,6 +223,7 @@ Flags by problem type: KColoring --graph, --k PartitionIntoTriangles --graph GraphPartitioning --graph + HamiltonianCircuit, HC --graph IsomorphicSpanningTree --graph, --tree Factoring --target, --m, --n BinPacking --sizes, --capacity diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index b89d65c9..1fcab23a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,7 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; -use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; +use problemreductions::models::graph::{GraphPartitioning, HamiltonianCircuit, HamiltonianPath}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, ShortestCommonSupersequence, SubsetSum, @@ -232,6 +232,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "QUBO" => "--matrix \"1,0.5;0.5,2\"", "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", + "HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0", "MinimumSumMulticenter" => { "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" } @@ -371,6 +372,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // Hamiltonian Circuit (graph only, no weights) + "HamiltonianCircuit" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create HamiltonianCircuit --graph 0-1,1-2,2-3,3-0" + ) + })?; + ( + ser(HamiltonianCircuit::new(graph))?, + resolved_variant.clone(), + ) + } + // Hamiltonian path (graph only, no weights) "HamiltonianPath" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -1546,6 +1560,17 @@ fn create_random( (ser(GraphPartitioning::new(graph))?, variant) } + // Hamiltonian Circuit (graph only, no weights) + "HamiltonianCircuit" => { + 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(HamiltonianCircuit::new(graph))?, variant) + } + // HamiltonianPath (graph only, no weights) "HamiltonianPath" => { let edge_prob = args.edge_prob.unwrap_or(0.5); @@ -1626,7 +1651,7 @@ fn create_random( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \ - OptimalLinearArrangement, HamiltonianPath)" + HamiltonianCircuit, OptimalLinearArrangement, HamiltonianPath)" ), }; diff --git a/src/lib.rs b/src/lib.rs index 1f1c99c3..579e31d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,8 @@ 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, HamiltonianPath, IsomorphicSpanningTree, SpinGlass, + BicliqueCover, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, + IsomorphicSpanningTree, SpinGlass, SubgraphIsomorphism, }; pub use crate::models::graph::{ diff --git a/src/models/graph/hamiltonian_circuit.rs b/src/models/graph/hamiltonian_circuit.rs new file mode 100644 index 00000000..21472167 --- /dev/null +++ b/src/models/graph/hamiltonian_circuit.rs @@ -0,0 +1,162 @@ +//! Hamiltonian Circuit problem implementation. +//! +//! The Hamiltonian Circuit problem asks whether a graph contains a cycle +//! that visits every vertex exactly once and returns to the starting vertex. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::variant::VariantParam; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "HamiltonianCircuit", + display_name: "Hamiltonian Circuit", + aliases: &["HC"], + dimensions: &[ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + ], + module_path: module_path!(), + description: "Does the graph contain a Hamiltonian circuit?", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, + ], + } +} + +/// The Hamiltonian Circuit problem. +/// +/// Given a graph G = (V, E), determine whether there exists a cycle that +/// visits every vertex exactly once and returns to the starting vertex. +/// +/// # Type Parameters +/// +/// * `G` - Graph type (e.g., SimpleGraph) +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::graph::HamiltonianCircuit; +/// use problemreductions::topology::SimpleGraph; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Square graph (4-cycle) has a Hamiltonian circuit +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); +/// let problem = HamiltonianCircuit::new(graph); +/// +/// let solver = BruteForce::new(); +/// let solutions = solver.find_all_satisfying(&problem); +/// +/// // Verify all solutions are valid Hamiltonian circuits +/// for sol in &solutions { +/// assert!(problem.evaluate(sol)); +/// } +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] +pub struct HamiltonianCircuit { + /// The underlying graph. + graph: G, +} + +impl HamiltonianCircuit { + /// Create a new Hamiltonian Circuit 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() + } +} + +impl Problem for HamiltonianCircuit +where + G: Graph + VariantParam, +{ + const NAME: &'static str = "HamiltonianCircuit"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G] + } + + fn dims(&self) -> Vec { + let n = self.graph.num_vertices(); + vec![n; n] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.graph.num_vertices(); + if n < 3 || 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 that consecutive vertices (including wrap-around) are connected by edges + for i in 0..n { + let u = config[i]; + let v = config[(i + 1) % n]; + if !self.graph.has_edge(u, v) { + return false; + } + } + + true + } +} + +impl SatisfactionProblem for HamiltonianCircuit {} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "hamiltonian_circuit_simplegraph", + build: || { + // Prism graph (triangular prism): 6 vertices, 9 edges + let problem = HamiltonianCircuit::new(SimpleGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 0), + (3, 4), + (4, 5), + (5, 3), + (0, 3), + (1, 4), + (2, 5), + ], + )); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 1, 2, 5, 4, 3]]) + }, + }] +} + +crate::declare_variants! { + default sat HamiltonianCircuit => "1.657^num_vertices", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/hamiltonian_circuit.rs"] +mod tests; diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 0b8a3ddf..1177398b 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -233,8 +233,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec1->2->5->4->3->0 + // Edges used: (0,1), (1,2), (2,5), (5,4), (4,3), (3,0) -- all present + assert!(problem.evaluate(&[0, 1, 2, 5, 4, 3])); + + // Invalid: 0->1->2->3 requires edge (2,3) which is NOT in the edge list + assert!(!problem.evaluate(&[0, 1, 2, 3, 4, 5])); + + // Invalid: duplicate vertex 0 -- not a valid permutation + assert!(!problem.evaluate(&[0, 0, 1, 2, 3, 4])); + + // Invalid: wrong-length config + assert!(!problem.evaluate(&[0, 1])); + + // Invalid: vertex out of range + assert!(!problem.evaluate(&[0, 1, 2, 3, 4, 99])); +} + +#[test] +fn test_hamiltonian_circuit_small_graphs() { + // Empty graph (0 vertices): n < 3, no circuit possible + let graph = SimpleGraph::new(0, vec![]); + let problem = HamiltonianCircuit::new(graph); + assert!(!problem.evaluate(&[])); + + // Single vertex: n < 3 + let graph = SimpleGraph::new(1, vec![]); + let problem = HamiltonianCircuit::new(graph); + assert!(!problem.evaluate(&[0])); + + // Two vertices with edge: n < 3 + let graph = SimpleGraph::new(2, vec![(0, 1)]); + let problem = HamiltonianCircuit::new(graph); + assert!(!problem.evaluate(&[0, 1])); + + // Triangle (K3): smallest valid Hamiltonian circuit + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let problem = HamiltonianCircuit::new(graph); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + // K3 has 6 directed Hamiltonian circuits: 3 rotations x 2 directions + assert_eq!(solutions.len(), 6); +} + +#[test] +fn test_hamiltonian_circuit_complete_graph_k4() { + // K4: complete graph on 4 vertices + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let problem = HamiltonianCircuit::new(graph); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + // K4 has 3 distinct undirected Hamiltonian circuits, each yielding + // 4 rotations x 2 directions = 8 directed permutations => 24 total + assert_eq!(solutions.len(), 24); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_hamiltonian_circuit_no_solution() { + // Path graph on 4 vertices: no Hamiltonian circuit possible + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let problem = HamiltonianCircuit::new(graph); + + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + +#[test] +fn test_hamiltonian_circuit_solver() { + // Cycle on 4 vertices (square): edges {0,1}, {1,2}, {2,3}, {3,0} + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let problem = HamiltonianCircuit::new(graph); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + + // 4-cycle has 8 Hamiltonian circuits: 4 starting positions x 2 directions + assert_eq!(solutions.len(), 8); + + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_hamiltonian_circuit_serialization() { + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let problem = HamiltonianCircuit::new(graph); + + let json = serde_json::to_string(&problem).unwrap(); + let restored: HamiltonianCircuit = serde_json::from_str(&json).unwrap(); + + assert_eq!(problem.dims(), restored.dims()); + + // Valid circuit gives the same result on both instances + assert_eq!( + problem.evaluate(&[0, 1, 2, 3]), + restored.evaluate(&[0, 1, 2, 3]) + ); + // Invalid config gives the same result on both instances + assert_eq!( + problem.evaluate(&[0, 0, 1, 2]), + restored.evaluate(&[0, 0, 1, 2]) + ); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ebbc68a0..8dde6dcd 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -99,6 +99,10 @@ fn test_all_problems_implement_trait_correctly() { ), "MinimumSumMulticenter", ); + check_problem_trait( + &HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])), + "HamiltonianCircuit", + ); check_problem_trait( &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), "HamiltonianPath",