diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6592594a..50adefd1 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -66,6 +66,7 @@ "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], "GraphPartitioning": [Graph Partitioning], + "HamiltonianCircuit": [Hamiltonian Circuit], "BiconnectivityAugmentation": [Biconnectivity Augmentation], "HamiltonianPath": [Hamiltonian Path], "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], @@ -595,6 +596,65 @@ Biconnectivity augmentation is a classical network-design problem: add backup li caption: [Biconnectivity Augmentation on a 6-vertex path with $B = 4$. Existing edges are black; green arcs show the selected augmentation $F'$ (total weight 4); dashed gray arcs are unselected candidates. The resulting graph $G' = (V, E union F')$ is biconnected.], ) ] +#{ + let x = load-model-example("HamiltonianCircuit") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let sol = x.optimal.at(0) + let circuit = sol.config + // Build circuit edges from consecutive vertices (including wrap-around) + let circuit-edges = range(circuit.len()).map(i => (circuit.at(i), circuit.at(calc.rem(i + 1, circuit.len())))) + [ + #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. + + *Example.* Consider the triangular prism graph $G$ on #nv vertices with #ne edges. The permutation $[#circuit.map(v => str(v)).join(", ")]$ forms a Hamiltonian circuit: each consecutive pair #circuit-edges.map(((u, v)) => $(#u, #v)$).join($,$) is an edge of $G$, and the path returns to the start. + + #figure({ + let blue = graph-colors.at(0) + let gray = luma(200) + canvas(length: 1cm, { + import draw: * + // Triangular prism: outer triangle + inner triangle + let r-out = 1.8 + let r-in = 0.9 + let verts = range(3).map(k => { + let angle = 90deg - k * 120deg + (calc.cos(angle) * r-out, calc.sin(angle) * r-out) + }) + range(3).map(k => { + let angle = 90deg - k * 120deg + (calc.cos(angle) * r-in, calc.sin(angle) * r-in) + }) + for (u, v) in edges { + let on-circuit = circuit-edges.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)) + g-edge(verts.at(u), verts.at(v), stroke: if on-circuit { 2pt + blue } else { 1pt + gray }) + } + for (k, pos) in verts.enumerate() { + g-node(pos, name: "v" + str(k), + fill: blue, + label: text(fill: white)[$v_#k$]) + } + }) + }, + caption: [Hamiltonian Circuit in the triangular prism graph. Blue edges show the circuit $#circuit.map(v => $v_#v$).join($arrow$) arrow v_#(circuit.at(0))$.], + ) + ] + ] +} + #problem-def("BoundedComponentSpanningForest")[ Given an undirected graph $G = (V, E)$ with vertex weights $w: V -> ZZ_(gt.eq 0)$, a positive integer $K <= |V|$, and a positive bound $B$, determine whether there exists a partition of $V$ into $t$ non-empty sets $V_1, dots, V_t$ with $1 <= t <= K$ such that each induced subgraph $G[V_i]$ is connected and each part satisfies $sum_(v in V_i) w(v) <= B$. diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 870a7b8f..db270576 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -311,6 +311,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}, @@ -322,17 +333,6 @@ @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}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c946abe4..09ac353c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -225,6 +225,7 @@ Flags by problem type: MinimumMultiwayCut --graph, --terminals, --edge-weights PartitionIntoTriangles --graph GraphPartitioning --graph + HamiltonianCircuit, HC --graph BoundedComponentSpanningForest --graph, --weights, --k, --bound UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 IsomorphicSpanningTree --graph, --tree diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 56da8a97..dcec61cc 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -9,8 +9,8 @@ use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{ - GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths, MinimumMultiwayCut, - MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, + GraphPartitioning, HamiltonianCircuit, HamiltonianPath, LengthBoundedDisjointPaths, + MinimumMultiwayCut, MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, @@ -299,6 +299,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" } @@ -634,6 +635,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(), + ) + } + // Biconnectivity augmentation "BiconnectivityAugmentation" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -3068,6 +3082,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); @@ -3219,7 +3244,7 @@ fn create_random( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \ - SteinerTree, OptimalLinearArrangement, HamiltonianPath)" + HamiltonianCircuit, SteinerTree, OptimalLinearArrangement, HamiltonianPath)" ), }; diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index cc0a8e78..dee92309 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -12,6 +12,7 @@ {"problem":"DirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"directed","edges":[[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":4,"sink_2":5,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true}],"optimal":[{"config":[0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,1,0,1,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,1,0,1,1,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,1,1,1,0,1],"metric":true},{"config":[0,1,0,1,0,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,1,1,1,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,1,0,1,1,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,1,0,1,1,1,0,0,0,0,1,0,0,0,1],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, + {"problem":"HamiltonianCircuit","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,0,null],[3,4,null],[4,5,null],[5,3,null],[0,3,null],[1,4,null],[2,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,1,2,5,4,3],"metric":true}],"optimal":[{"config":[0,1,2,5,4,3],"metric":true},{"config":[0,1,4,3,5,2],"metric":true},{"config":[0,2,1,4,5,3],"metric":true},{"config":[0,2,5,3,4,1],"metric":true},{"config":[0,3,4,5,2,1],"metric":true},{"config":[0,3,5,4,1,2],"metric":true},{"config":[1,0,2,5,3,4],"metric":true},{"config":[1,0,3,4,5,2],"metric":true},{"config":[1,2,0,3,5,4],"metric":true},{"config":[1,2,5,4,3,0],"metric":true},{"config":[1,4,3,5,2,0],"metric":true},{"config":[1,4,5,3,0,2],"metric":true},{"config":[2,0,1,4,3,5],"metric":true},{"config":[2,0,3,5,4,1],"metric":true},{"config":[2,1,0,3,4,5],"metric":true},{"config":[2,1,4,5,3,0],"metric":true},{"config":[2,5,3,4,1,0],"metric":true},{"config":[2,5,4,3,0,1],"metric":true},{"config":[3,0,1,2,5,4],"metric":true},{"config":[3,0,2,1,4,5],"metric":true},{"config":[3,4,1,0,2,5],"metric":true},{"config":[3,4,5,2,1,0],"metric":true},{"config":[3,5,2,0,1,4],"metric":true},{"config":[3,5,4,1,2,0],"metric":true},{"config":[4,1,0,2,5,3],"metric":true},{"config":[4,1,2,0,3,5],"metric":true},{"config":[4,3,0,1,2,5],"metric":true},{"config":[4,3,5,2,0,1],"metric":true},{"config":[4,5,2,1,0,3],"metric":true},{"config":[4,5,3,0,2,1],"metric":true},{"config":[5,2,0,1,4,3],"metric":true},{"config":[5,2,1,0,3,4],"metric":true},{"config":[5,3,0,2,1,4],"metric":true},{"config":[5,3,4,1,0,2],"metric":true},{"config":[5,4,1,2,0,3],"metric":true},{"config":[5,4,3,0,1,2],"metric":true}]}, {"problem":"HamiltonianPath","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[3,4,null],[3,5,null],[4,2,null],[5,1,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,2,4,3,1,5],"metric":true}],"optimal":[{"config":[0,1,5,3,2,4],"metric":true},{"config":[0,1,5,3,4,2],"metric":true},{"config":[0,2,4,3,1,5],"metric":true},{"config":[0,2,4,3,5,1],"metric":true},{"config":[1,0,2,4,3,5],"metric":true},{"config":[1,5,3,4,2,0],"metric":true},{"config":[2,0,1,5,3,4],"metric":true},{"config":[2,4,3,5,1,0],"metric":true},{"config":[3,4,2,0,1,5],"metric":true},{"config":[3,5,1,0,2,4],"metric":true},{"config":[4,2,0,1,3,5],"metric":true},{"config":[4,2,0,1,5,3],"metric":true},{"config":[4,2,3,5,1,0],"metric":true},{"config":[4,3,2,0,1,5],"metric":true},{"config":[4,3,5,1,0,2],"metric":true},{"config":[5,1,0,2,3,4],"metric":true},{"config":[5,1,0,2,4,3],"metric":true},{"config":[5,1,3,4,2,0],"metric":true},{"config":[5,3,1,0,2,4],"metric":true},{"config":[5,3,4,2,0,1],"metric":true}]}, {"problem":"ILP","variant":{"variable":"i32"},"instance":{"constraints":[{"cmp":"Le","rhs":5.0,"terms":[[0,1.0],[1,1.0]]},{"cmp":"Le","rhs":28.0,"terms":[[0,4.0],[1,7.0]]}],"num_vars":2,"objective":[[0,-5.0],[1,-6.0]],"sense":"Minimize"},"samples":[{"config":[0,4],"metric":{"Valid":-24.0}}],"optimal":[{"config":[3,2],"metric":{"Valid":-27.0}}]}, {"problem":"IsomorphicSpanningTree","variant":{},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"tree":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[0,1,2,3],"metric":true}],"optimal":[{"config":[0,1,2,3],"metric":true},{"config":[0,1,3,2],"metric":true},{"config":[0,2,1,3],"metric":true},{"config":[0,2,3,1],"metric":true},{"config":[0,3,1,2],"metric":true},{"config":[0,3,2,1],"metric":true},{"config":[1,0,2,3],"metric":true},{"config":[1,0,3,2],"metric":true},{"config":[1,2,0,3],"metric":true},{"config":[1,2,3,0],"metric":true},{"config":[1,3,0,2],"metric":true},{"config":[1,3,2,0],"metric":true},{"config":[2,0,1,3],"metric":true},{"config":[2,0,3,1],"metric":true},{"config":[2,1,0,3],"metric":true},{"config":[2,1,3,0],"metric":true},{"config":[2,3,0,1],"metric":true},{"config":[2,3,1,0],"metric":true},{"config":[3,0,1,2],"metric":true},{"config":[3,0,2,1],"metric":true},{"config":[3,1,0,2],"metric":true},{"config":[3,1,2,0],"metric":true},{"config":[3,2,0,1],"metric":true},{"config":[3,2,1,0],"metric":true}]}, diff --git a/src/lib.rs b/src/lib.rs index 8af9c695..1bcad8a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,8 +47,8 @@ pub mod prelude { pub use crate::models::graph::{ BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GraphPartitioning, - HamiltonianPath, IsomorphicSpanningTree, LengthBoundedDisjointPaths, SpinGlass, - SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, LengthBoundedDisjointPaths, + SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/hamiltonian_circuit.rs b/src/models/graph/hamiltonian_circuit.rs new file mode 100644 index 00000000..49abad33 --- /dev/null +++ b/src/models/graph/hamiltonian_circuit.rs @@ -0,0 +1,176 @@ +//! 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() + } + + /// Check if a configuration is a valid Hamiltonian circuit. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + is_valid_hamiltonian_circuit(&self.graph, config) + } +} + +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 { + is_valid_hamiltonian_circuit(&self.graph, config) + } +} + +/// Check if a configuration represents a valid Hamiltonian circuit in the graph. +/// +/// A valid Hamiltonian circuit is a permutation of the vertices such that +/// consecutive vertices in the permutation are adjacent in the graph, +/// including a closing edge from the last vertex back to the first. +pub(crate) fn is_valid_hamiltonian_circuit(graph: &G, config: &[usize]) -> bool { + let n = 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 !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/mod.rs b/src/models/graph/mod.rs index b8da19aa..7bdb4fea 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -9,6 +9,7 @@ //! - [`MaximumClique`]: Maximum weight clique //! - [`MaxCut`]: Maximum cut on weighted graphs //! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) +//! - [`HamiltonianCircuit`]: Hamiltonian circuit (decision problem) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) //! - [`KColoring`]: K-vertex coloring //! - [`PartitionIntoTriangles`]: Partition vertices into triangles @@ -39,6 +40,7 @@ pub(crate) mod biconnectivity_augmentation; pub(crate) mod bounded_component_spanning_forest; pub(crate) mod directed_two_commodity_integral_flow; pub(crate) mod graph_partitioning; +pub(crate) mod hamiltonian_circuit; pub(crate) mod hamiltonian_path; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kcoloring; @@ -71,6 +73,7 @@ pub use biconnectivity_augmentation::BiconnectivityAugmentation; pub use bounded_component_spanning_forest::BoundedComponentSpanningForest; pub use directed_two_commodity_integral_flow::DirectedTwoCommodityIntegralFlow; pub use graph_partitioning::GraphPartitioning; +pub use hamiltonian_circuit::HamiltonianCircuit; pub use hamiltonian_path::HamiltonianPath; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kcoloring::KColoring; @@ -103,6 +106,7 @@ pub(crate) fn canonical_model_example_specs() -> 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 a142dc4a..2afba773 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -102,6 +102,33 @@ fn test_all_problems_implement_trait_correctly() { ), "StrongConnectivityAugmentation", ); + 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", + ); + check_problem_trait( + &OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 3), + "OptimalLinearArrangement", + ); + check_problem_trait( + &IsomorphicSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + ), + "IsomorphicSpanningTree", + ); + check_problem_trait( + &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3), + "ShortestCommonSupersequence", + ); + check_problem_trait( + &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), + "FlowShopScheduling", + ); check_problem_trait( &SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]), "SequencingWithReleaseTimesAndDeadlines",