diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 616a03a6e..c6b5d91f2 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -149,6 +149,14 @@ Required tests: - `test__direction` -- verify optimization direction (if optimization problem) - `test__serialization` -- round-trip serde test (optional but recommended) - `test__solver` -- verify brute-force solver finds correct solutions +- `test__paper_example` -- **use the same instance from the paper example** (Step 6), verify the claimed solution is valid/optimal and the solution count matches + +The `test__paper_example` test is critical for consistency between code and paper. It must: +1. Construct the exact same instance shown in the paper's example figure +2. Evaluate the solution shown in the paper and assert it is valid (and optimal for optimization problems) +3. Use `BruteForce` to find all optimal/satisfying solutions and assert the count matches the paper's claim + +This test should be written **after** Step 6 (paper entry), once the example instance and solution are finalized. If writing tests before the paper, use the same instance you plan to use in the paper and come back to verify consistency. Link the test file via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the model file. @@ -233,3 +241,4 @@ If running standalone (not inside `make run-plan`), invoke [review-implementatio | Missing from CLI help table | Must add entry to "Flags by problem type" table in `cli.rs` `after_help` | | Schema lists derived fields | Schema should list constructor params, not internal fields (e.g., `matrix, k` not `matrix, m, n, k`) | | Forgetting trait_consistency | Must add entry in `test_all_problems_implement_trait_correctly` (and `test_direction` for optimization) in `src/unit_tests/trait_consistency.rs` | +| Paper example not tested | Must include `test__paper_example` that verifies the exact instance, solution, and solution count shown in the paper | diff --git a/Cargo.toml b/Cargo.toml index 938ff968b..21c81f18f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ bitvec = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" +num-bigint = "0.4" num-traits = "0.2" good_lp = { version = "1.8", default-features = false, optional = true } inventory = "0.3" diff --git a/docs/paper/examples/maximumindependentset_to_maximumclique.json b/docs/paper/examples/maximumindependentset_to_maximumclique.json index 21a8853eb..352ddeb68 100644 --- a/docs/paper/examples/maximumindependentset_to_maximumclique.json +++ b/docs/paper/examples/maximumindependentset_to_maximumclique.json @@ -31,8 +31,8 @@ "target": { "problem": "MaximumClique", "variant": { - "weight": "i32", - "graph": "SimpleGraph" + "graph": "SimpleGraph", + "weight": "i32" }, "instance": { "edges": [ diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 70827d5cd..da24a38ee 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -64,6 +64,7 @@ "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], + "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], ) @@ -451,6 +452,32 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co 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$. + + *Example.* Consider the graph $G$ on 6 vertices with edges ${(0,1), (0,2), (1,3), (2,3), (3,4), (3,5), (2,4), (1,5)}$. The sequence $[0, 2, 4, 3, 1, 5]$ is a Hamiltonian path: it visits every vertex exactly once, and each consecutive pair is adjacent — $(0,2), (2,4), (4,3), (3,1), (1,5) in E$. + + #figure({ + let blue = graph-colors.at(0) + let gray = luma(200) + canvas(length: 1cm, { + import draw: * + // 6 vertices in two rows + let verts = ((0, 1.5), (1.5, 1.5), (3, 1.5), (1.5, 0), (3, 0), (0, 0)) + let edges = ((0,1),(0,2),(1,3),(2,3),(3,4),(3,5),(2,4),(1,5)) + // Hamiltonian path edges: 0-2, 2-4, 4-3, 3-1, 1-5 + let path-edges = ((0,2),(2,4),(4,3),(3,1),(1,5)) + for (u, v) in edges { + let on-path = path-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-path { 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 Path in a 6-vertex graph. Blue edges show the path $v_0 arrow v_2 arrow v_4 arrow v_3 arrow v_1 arrow v_5$.], + ) ] #problem-def("IsomorphicSpanningTree")[ Given a graph $G = (V, E)$ and a tree $T = (V_T, E_T)$ with $|V| = |V_T|$, determine whether $G$ contains a spanning tree isomorphic to $T$: does there exist a bijection $pi: V_T -> V$ such that for every edge ${u, v} in E_T$, ${pi(u), pi(v)} in E$? @@ -655,6 +682,31 @@ Also known as the _p-median problem_. This is a classical NP-complete facility l The best known exact algorithm runs in $O^*(2^n)$ time by brute-force enumeration of all $binom(n, K)$ vertex subsets. Constant-factor approximation algorithms exist: Charikar et al. (1999) gave the first constant-factor result, and the best known ratio is $(2 + epsilon)$ by Cohen-Addad et al. (STOC 2022). Variables: $n = |V|$ binary variables, one per vertex. $x_v = 1$ if vertex $v$ is selected as a center. A configuration is valid when exactly $K$ centers are selected and all vertices are reachable from at least one center. + + *Example.* Consider the graph $G$ on 7 vertices with unit weights $w(v) = 1$ and unit edge lengths, edges ${(0,1), (1,2), (2,3), (3,4), (4,5), (5,6), (0,6), (2,5)}$, and $K = 2$. Placing centers at $P = {v_2, v_5}$ gives distances $d(v_0) = 2$, $d(v_1) = 1$, $d(v_2) = 0$, $d(v_3) = 1$, $d(v_4) = 1$, $d(v_5) = 0$, $d(v_6) = 1$, for a total cost of $2 + 1 + 0 + 1 + 1 + 0 + 1 = 6$. This is optimal. + + #figure({ + let blue = graph-colors.at(0) + let gray = luma(200) + canvas(length: 1cm, { + import draw: * + // 7 vertices on a rough circle + let verts = ((-1.5, 0.8), (0, 1.5), (1.5, 0.8), (1.5, -0.8), (0, -1.5), (-1.5, -0.8), (-2.2, 0)) + let edges = ((0,1),(1,2),(2,3),(3,4),(4,5),(5,6),(0,6),(2,5)) + for (u, v) in edges { + g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray) + } + let centers = (2, 5) + for (k, pos) in verts.enumerate() { + let is-center = centers.any(c => c == k) + g-node(pos, name: "v" + str(k), + fill: if is-center { blue } else { white }, + label: if is-center { text(fill: white)[$v_#k$] } else { [$v_#k$] }) + } + }) + }, + caption: [Minimum Sum Multicenter with $K = 2$ on a 7-vertex graph. Centers $v_2$ and $v_5$ (blue) achieve optimal total weighted distance 6.], + ) ] == Set Problems @@ -1019,6 +1071,37 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa ) ] +#problem-def("PartitionIntoTriangles")[ + Given a graph $G = (V, E)$ with $|V| = 3q$ for some integer $q$, determine whether the vertices of $G$ can be partitioned into $q$ disjoint triples $V_1, dots, V_q$, each containing exactly 3 vertices, such that for each $V_i = {u_i, v_i, w_i}$, all three edges ${u_i, v_i}$, ${u_i, w_i}$, and ${v_i, w_i}$ belong to $E$. +][ + Partition Into Triangles is NP-complete by transformation from 3-Dimensional Matching @garey1979[GT11]. It remains NP-complete on graphs of maximum degree 4, with an exact algorithm running in $O^*(1.0222^n)$ for bounded-degree-4 graphs @vanrooij2013. The general brute-force bound is $O^*(2^n)$#footnote[No algorithm improving on brute-force enumeration is known for general Partition Into Triangles.]. + + *Example.* Consider $G$ with $n = 6$ vertices ($q = 2$) and edges ${0,1}$, ${0,2}$, ${1,2}$, ${3,4}$, ${3,5}$, ${4,5}$, ${0,3}$. The partition $V_1 = {v_0, v_1, v_2}$, $V_2 = {v_3, v_4, v_5}$ is valid: $V_1$ forms a triangle (edges ${0,1}$, ${0,2}$, ${1,2}$ all present) and $V_2$ forms a triangle (edges ${3,4}$, ${3,5}$, ${4,5}$ all present). The cross-edge ${0,3}$ is unused. Swapping $v_2$ and $v_3$ yields $V'_1 = {v_0, v_1, v_3}$, which fails because ${1, 3} in.not E$. + + #figure( + canvas(length: 1cm, { + import draw: * + // Two triangles side by side with a cross-edge + let verts = ((0, 1.2), (1, 0), (-1, 0), (3, 1.2), (4, 0), (2, 0)) + let edges = ((0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (0, 3)) + let tri1 = (0, 1, 2) + let tri2 = (3, 4, 5) + // Draw edges + for (u, v) in edges { + let is-cross = u == 0 and v == 3 + g-edge(verts.at(u), verts.at(v), + stroke: if is-cross { 1pt + luma(180) } else if tri1.contains(u) and tri1.contains(v) { 1.5pt + graph-colors.at(0) } else { 1.5pt + rgb("#76b7b2") }) + } + // Draw vertices + for (k, p) in verts.enumerate() { + let c = if tri1.contains(k) { graph-colors.at(0).lighten(70%) } else { rgb("#76b7b2").lighten(70%) } + g-node(p, name: "v" + str(k), fill: c, label: $v_#k$) + } + }), + caption: [Partition Into Triangles: $V_1 = {v_0, v_1, v_2}$ (blue) and $V_2 = {v_3, v_4, v_5}$ (teal) each form a triangle. The cross-edge $(v_0, v_3)$ (gray) is unused.], + ) +] + #problem-def("BinPacking")[ Given $n$ items with sizes $s_1, dots, s_n in RR^+$ and bin capacity $C > 0$, find an assignment $x: {1, dots, n} -> NN$ minimizing $|{x(i) : i = 1, dots, n}|$ (the number of distinct bins used) subject to $forall j: sum_(i: x(i) = j) s_i lt.eq C$. ][ @@ -1965,6 +2048,39 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ For each position $k$, find vertex $v$ with $x_(v,k) = 1$ to recover the tour permutation; then select edges between consecutive positions. ] +#let tsp_qubo = load-example("travelingsalesman_to_qubo") +#let tsp_qubo_r = load-results("travelingsalesman_to_qubo") +#let tsp_qubo_sol = tsp_qubo_r.solutions.at(0) + +#reduction-rule("TravelingSalesman", "QUBO", + example: true, + example-caption: [TSP on $K_3$ with weights $w_(01) = 1$, $w_(02) = 2$, $w_(12) = 3$: the QUBO ground state encodes the optimal tour with cost $1 + 2 + 3 = 6$.], + extra: [ + *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.num_vars$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. + $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) underbrace(x_(2,0) x_(2,1) x_(2,2), "vertex 2") $ + + *Step 2 -- Penalize invalid permutations.* The penalty $A = 1 + |w_(01)| + |w_(02)| + |w_(12)| = 1 + 1 + 2 + 3 = 7$ ensures any row/column constraint violation outweighs any tour cost. Row constraints (each vertex at exactly one position) and column constraints (each position has one vertex) contribute diagonal $-7$ and off-diagonal $+14$ within each group.\ + + *Step 3 -- Encode edge costs.* For each edge $(u,v)$ and position $p$, the products $x_(u,p) x_(v,(p+1) mod 3)$ and $x_(v,p) x_(u,(p+1) mod 3)$ add the edge weight $w_(u v)$ when vertices $u,v$ are consecutive in the tour. Since $K_3$ is complete, all pairs are edges with their actual weights.\ + + *Step 4 -- Verify a solution.* The QUBO ground state $bold(x) = (#tsp_qubo_sol.target_config.map(str).join(", "))$ encodes a valid tour. Reading the permutation: each 3-bit group has exactly one 1 (valid permutation #sym.checkmark). The tour cost equals $w_(01) + w_(02) + w_(12) = 1 + 2 + 3 = 6$.\ + + *Count:* #tsp_qubo_r.solutions.len() optimal QUBO solutions $= 3! = 6$. On $K_3$ with distinct edge weights $1, 2, 3$, every Hamiltonian cycle has cost $1 + 2 + 3 = 6$ (all edges used), and 3 cyclic tours $times$ 2 directions yield $6$ permutation matrices. + ], +)[ + Position-based QUBO encoding @lucas2014 maps a Hamiltonian tour to $n^2$ binary variables $x_(v,p)$, where $x_(v,p) = 1$ iff city $v$ is visited at position $p$. The QUBO Hamiltonian $H = H_A + H_B + H_C$ combines permutation constraints with the distance objective ($n^2$ variables indexed by $v dot n + p$). +][ + _Construction._ For graph $G = (V, E)$ with $n = |V|$ and edge weights $w_(u v)$. Let $A = 1 + sum_((u,v) in E) |w_(u v)|$ be the penalty coefficient. + + _Variables:_ Binary $x_(v,p) in {0, 1}$ for vertex $v in V$ and position $p in {0, dots, n-1}$. QUBO variable index: $v dot n + p$. + + _QUBO matrix:_ (1) Row constraint $H_A = A sum_v (1 - sum_p x_(v,p))^2$: diagonal $Q[v n + p, v n + p] += -A$, off-diagonal $Q[v n + p, v n + p'] += 2A$ for $p < p'$. (2) Column constraint $H_B = A sum_p (1 - sum_v x_(v,p))^2$: symmetric to $H_A$. (3) Distance $H_C = sum_((u,v) in E) w_(u v) sum_p (x_(u,p) x_(v,(p+1) mod n) + x_(v,p) x_(u,(p+1) mod n))$. For non-edges, penalty $A$ replaces $w_(u v)$. + + _Correctness._ ($arrow.r.double$) A valid tour defines a permutation matrix satisfying $H_A = H_B = 0$; the $H_C$ terms sum to the tour cost. ($arrow.l.double$) The minimum-energy state has $H_A = H_B = 0$ (penalty $A$ exceeds any tour cost), so it encodes a valid permutation; $H_C$ equals the tour cost, selecting the shortest tour. + + _Solution extraction._ From QUBO solution $x^*$, for each position $p$ find the unique vertex $v$ with $x^*_(v n + p) = 1$. Map consecutive position pairs to edge indices. +] + #reduction-rule("LongestCommonSubsequence", "ILP")[ The match-pair ILP formulation @blum2021 encodes subsequence alignment as a binary optimization. For two strings $s_1$ (length $n_1$) and $s_2$ (length $n_2$), each position pair $(j_1, j_2)$ where $s_1[j_1] = s_2[j_2]$ yields a binary variable. Constraints enforce one-to-one matching and order preservation (no crossings). The objective maximizes the number of matched pairs. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 505532c49..64bd0b76b 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1,3 +1,34 @@ +@article{juttner2018, + author = {Alpár Jüttner and Péter Madarasi}, + title = {VF2++ — An improved subgraph isomorphism algorithm}, + journal = {Discrete Applied Mathematics}, + volume = {242}, + pages = {69--81}, + year = {2018}, + doi = {10.1016/j.dam.2018.02.018} +} + +@article{johnson1954, + author = {Selmer M. Johnson}, + title = {Optimal two- and three-stage production schedules with setup times included}, + journal = {Naval Research Logistics Quarterly}, + volume = {1}, + number = {1}, + pages = {61--68}, + year = {1954}, + doi = {10.1002/nav.3800010110} +} + +@article{shang2018, + author = {Lei Shang and Chao Wan and Jianan Wang}, + title = {An exact algorithm for the three-machine flow shop problem}, + journal = {Computers \& Operations Research}, + volume = {91}, + pages = {79--89}, + year = {2018}, + doi = {10.1016/j.cor.2017.10.015} +} + @inproceedings{karp1972, author = {Richard M. Karp}, title = {Reducibility among Combinatorial Problems}, @@ -118,6 +149,17 @@ @article{robson2001 note = {Technical Report 1251-01, LaBRI, Université Bordeaux I} } +@article{vanrooij2013, + author = {Johan M. M. van Rooij and Marcel van Kooten Niekerk and Hans L. Bodlaender}, + title = {Partition Into Triangles on Bounded Degree Graphs}, + journal = {Theory of Computing Systems}, + volume = {52}, + number = {4}, + pages = {687--718}, + year = {2013}, + doi = {10.1007/s00224-012-9412-5} +} + @article{vanrooij2011, author = {Johan M. M. van Rooij and Hans L. Bodlaender}, title = {Exact algorithms for dominating set}, @@ -543,6 +585,50 @@ @article{lucchesi1978 doi = {10.1112/jlms/s2-17.3.369} } +@article{lenstra1976, + author = {Jan Karel Lenstra and Alexander H. G. Rinnooy Kan}, + title = {On General Routing Problems}, + journal = {Networks}, + volume = {6}, + number = {3}, + pages = {273--280}, + year = {1976}, + doi = {10.1002/net.3230060305} +} + +@article{frederickson1979, + author = {Greg N. Frederickson}, + title = {Approximation Algorithms for Some Postman Problems}, + journal = {Journal of the ACM}, + volume = {26}, + number = {3}, + pages = {538--554}, + year = {1979}, + doi = {10.1145/322139.322150} +} + +@article{alon1995, + author = {Noga Alon and Raphael Yuster and Uri Zwick}, + title = {Color-coding}, + journal = {Journal of the ACM}, + volume = {42}, + number = {4}, + pages = {844--856}, + year = {1995}, + doi = {10.1145/210332.210337} +} + +@inproceedings{cordella2004, + author = {Luigi P. Cordella and Pasquale Foggia and Carlo Sansone and Mario Vento}, + title = {A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs}, + booktitle = {IEEE Transactions on Pattern Analysis and Machine Intelligence}, + volume = {26}, + number = {10}, + pages = {1367--1372}, + year = {2004}, + doi = {10.1109/TPAMI.2004.75} +} + @article{papadimitriou1982, author = {Christos H. Papadimitriou and Mihalis Yannakakis}, title = {The Complexity of Restricted Spanning Tree Problems}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index d46b150ae..078dea0a6 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -110,6 +110,27 @@ } ] }, + { + "name": "FlowShopScheduling", + "description": "Determine if a flow-shop schedule for jobs on m processors meets a deadline", + "fields": [ + { + "name": "num_processors", + "type_name": "usize", + "description": "Number of machines m" + }, + { + "name": "task_lengths", + "type_name": "Vec>", + "description": "task_lengths[j][i] = length of job j's task on machine i" + }, + { + "name": "deadline", + "type_name": "u64", + "description": "Global deadline D" + } + ] + }, { "name": "GraphPartitioning", "description": "Find minimum cut balanced bisection of a graph", @@ -345,6 +366,22 @@ } ] }, + { + "name": "MinimumFeedbackArcSet", + "description": "Find minimum weight feedback arc set in a directed graph", + "fields": [ + { + "name": "graph", + "type_name": "DirectedGraph", + "description": "The directed graph G=(V,A)" + }, + { + "name": "weights", + "type_name": "Vec", + "description": "Arc weights w: A -> R" + } + ] + }, { "name": "MinimumFeedbackVertexSet", "description": "Find minimum weight feedback vertex set in a directed graph", @@ -580,16 +617,16 @@ }, { "name": "SubsetSum", - "description": "Find a subset of integers that sums to exactly a target value", + "description": "Find a subset of positive integers that sums to exactly a target value", "fields": [ { "name": "sizes", - "type_name": "Vec", - "description": "Integer sizes s(a) for each element" + "type_name": "Vec", + "description": "Positive integer sizes s(a) for each element" }, { "name": "target", - "type_name": "i64", + "type_name": "BigUint", "description": "Target sum B" } ] diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 59ea473f2..e8a63e10e 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -64,6 +64,13 @@ "doc_path": "models/misc/struct.Factoring.html", "complexity": "exp((m + n)^(1/3) * log(m + n)^(2/3))" }, + { + "name": "FlowShopScheduling", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.FlowShopScheduling.html", + "complexity": "factorial(num_jobs)" + }, { "name": "GraphPartitioning", "variant": { @@ -73,6 +80,15 @@ "doc_path": "models/graph/struct.GraphPartitioning.html", "complexity": "2^num_vertices" }, + { + "name": "HamiltonianPath", + "variant": { + "graph": "SimpleGraph" + }, + "category": "graph", + "doc_path": "models/graph/struct.HamiltonianPath.html", + "complexity": "1.657^num_vertices" + }, { "name": "ILP", "variant": { @@ -91,6 +107,13 @@ "doc_path": "models/algebraic/struct.ILP.html", "complexity": "num_vars^num_vars" }, + { + "name": "IsomorphicSpanningTree", + "variant": {}, + "category": "graph", + "doc_path": "models/graph/struct.IsomorphicSpanningTree.html", + "complexity": "factorial(num_vertices)" + }, { "name": "KColoring", "variant": { @@ -175,6 +198,13 @@ "doc_path": "models/misc/struct.Knapsack.html", "complexity": "2^(num_items / 2)" }, + { + "name": "LongestCommonSubsequence", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.LongestCommonSubsequence.html", + "complexity": "2^min_string_length" + }, { "name": "MaxCut", "variant": { @@ -322,6 +352,15 @@ "doc_path": "models/graph/struct.MinimumDominatingSet.html", "complexity": "1.4969^num_vertices" }, + { + "name": "MinimumFeedbackArcSet", + "variant": { + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.MinimumFeedbackArcSet.html", + "complexity": "2^num_vertices" + }, { "name": "MinimumFeedbackVertexSet", "variant": { @@ -340,6 +379,16 @@ "doc_path": "models/set/struct.MinimumSetCovering.html", "complexity": "2^num_sets" }, + { + "name": "MinimumSumMulticenter", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.MinimumSumMulticenter.html", + "complexity": "2^num_vertices" + }, { "name": "MinimumVertexCover", "variant": { @@ -350,6 +399,15 @@ "doc_path": "models/graph/struct.MinimumVertexCover.html", "complexity": "1.1996^num_vertices" }, + { + "name": "OptimalLinearArrangement", + "variant": { + "graph": "SimpleGraph" + }, + "category": "graph", + "doc_path": "models/graph/struct.OptimalLinearArrangement.html", + "complexity": "2^num_vertices" + }, { "name": "PaintShop", "variant": {}, @@ -357,6 +415,15 @@ "doc_path": "models/misc/struct.PaintShop.html", "complexity": "2^num_cars" }, + { + "name": "PartitionIntoTriangles", + "variant": { + "graph": "SimpleGraph" + }, + "category": "graph", + "doc_path": "models/graph/struct.PartitionIntoTriangles.html", + "complexity": "2^num_vertices" + }, { "name": "QUBO", "variant": { @@ -366,6 +433,16 @@ "doc_path": "models/algebraic/struct.QUBO.html", "complexity": "2^num_vars" }, + { + "name": "RuralPostman", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.RuralPostman.html", + "complexity": "2^num_vertices * num_vertices^2" + }, { "name": "Satisfiability", "variant": {}, @@ -373,6 +450,13 @@ "doc_path": "models/formula/struct.Satisfiability.html", "complexity": "2^num_variables" }, + { + "name": "ShortestCommonSupersequence", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.ShortestCommonSupersequence.html", + "complexity": "alphabet_size ^ bound" + }, { "name": "SpinGlass", "variant": { @@ -393,6 +477,13 @@ "doc_path": "models/graph/struct.SpinGlass.html", "complexity": "2^num_spins" }, + { + "name": "SubgraphIsomorphism", + "variant": {}, + "category": "graph", + "doc_path": "models/graph/struct.SubgraphIsomorphism.html", + "complexity": "num_host_vertices ^ num_pattern_vertices" + }, { "name": "SubsetSum", "variant": {}, @@ -414,7 +505,7 @@ "edges": [ { "source": 3, - "target": 9, + "target": 11, "overhead": [ { "field": "num_vars", @@ -429,7 +520,7 @@ }, { "source": 4, - "target": 9, + "target": 11, "overhead": [ { "field": "num_vars", @@ -444,7 +535,7 @@ }, { "source": 4, - "target": 42, + "target": 52, "overhead": [ { "field": "num_spins", @@ -474,7 +565,7 @@ }, { "source": 7, - "target": 10, + "target": 12, "overhead": [ { "field": "num_vars", @@ -488,8 +579,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 9, - "target": 10, + "source": 11, + "target": 12, "overhead": [ { "field": "num_vars", @@ -503,8 +594,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 9, - "target": 39, + "source": 11, + "target": 47, "overhead": [ { "field": "num_vars", @@ -514,8 +605,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 12, - "target": 15, + "source": 15, + "target": 18, "overhead": [ { "field": "num_vertices", @@ -529,8 +620,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 15, - "target": 9, + "source": 18, + "target": 11, "overhead": [ { "field": "num_vars", @@ -544,8 +635,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 15, - "target": 39, + "source": 18, + "target": 47, "overhead": [ { "field": "num_vars", @@ -555,8 +646,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 16, - "target": 18, + "source": 19, + "target": 21, "overhead": [ { "field": "num_vars", @@ -570,8 +661,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 16, - "target": 39, + "source": 19, + "target": 47, "overhead": [ { "field": "num_vars", @@ -581,8 +672,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 17, - "target": 18, + "source": 20, + "target": 21, "overhead": [ { "field": "num_vars", @@ -596,8 +687,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 17, - "target": 39, + "source": 20, + "target": 47, "overhead": [ { "field": "num_vars", @@ -607,8 +698,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 17, - "target": 43, + "source": 20, + "target": 54, "overhead": [ { "field": "num_elements", @@ -618,8 +709,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 18, - "target": 40, + "source": 21, + "target": 49, "overhead": [ { "field": "num_clauses", @@ -637,8 +728,23 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 20, - "target": 42, + "source": 23, + "target": 11, + "overhead": [ + { + "field": "num_vars", + "formula": "num_chars_first * num_chars_second" + }, + { + "field": "num_constraints", + "formula": "num_chars_first + num_chars_second + (num_chars_first * num_chars_second)^2" + } + ], + "doc_path": "rules/longestcommonsubsequence_ilp/index.html" + }, + { + "source": 24, + "target": 52, "overhead": [ { "field": "num_spins", @@ -652,8 +758,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 22, - "target": 9, + "source": 26, + "target": 11, "overhead": [ { "field": "num_vars", @@ -667,8 +773,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 22, - "target": 26, + "source": 26, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -682,8 +788,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 23, - "target": 24, + "source": 27, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -697,8 +803,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 23, - "target": 28, + "source": 27, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -712,8 +818,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 24, - "target": 29, + "source": 28, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -727,8 +833,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 23, + "source": 29, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -742,8 +848,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 25, - "target": 26, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -757,8 +863,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 25, - "target": 27, + "source": 29, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -772,8 +878,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 25, - "target": 31, + "source": 29, + "target": 35, "overhead": [ { "field": "num_sets", @@ -787,8 +893,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 26, - "target": 22, + "source": 30, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -802,8 +908,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 26, - "target": 33, + "source": 30, + "target": 37, "overhead": [ { "field": "num_sets", @@ -817,8 +923,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 26, - "target": 37, + "source": 30, + "target": 43, "overhead": [ { "field": "num_vertices", @@ -832,8 +938,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 27, - "target": 29, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -847,8 +953,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 25, + "source": 32, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -862,8 +968,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 29, + "source": 32, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -877,8 +983,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 26, + "source": 33, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -892,8 +998,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 9, + "source": 34, + "target": 11, "overhead": [ { "field": "num_vars", @@ -907,8 +1013,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 30, - "target": 33, + "source": 34, + "target": 37, "overhead": [ { "field": "num_sets", @@ -922,8 +1028,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 31, - "target": 25, + "source": 35, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -937,8 +1043,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 33, + "source": 35, + "target": 37, "overhead": [ { "field": "num_sets", @@ -952,8 +1058,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 32, - "target": 39, + "source": 36, + "target": 47, "overhead": [ { "field": "num_vars", @@ -963,8 +1069,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 33, - "target": 9, + "source": 37, + "target": 11, "overhead": [ { "field": "num_vars", @@ -978,8 +1084,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 33, - "target": 26, + "source": 37, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -993,8 +1099,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 33, - "target": 32, + "source": 37, + "target": 36, "overhead": [ { "field": "num_sets", @@ -1008,8 +1114,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 34, - "target": 9, + "source": 38, + "target": 11, "overhead": [ { "field": "num_vars", @@ -1023,8 +1129,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 36, - "target": 9, + "source": 41, + "target": 11, "overhead": [ { "field": "num_vars", @@ -1038,8 +1144,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 37, - "target": 26, + "source": 43, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -1053,8 +1159,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 37, - "target": 36, + "source": 43, + "target": 41, "overhead": [ { "field": "num_sets", @@ -1068,8 +1174,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 39, - "target": 9, + "source": 47, + "target": 11, "overhead": [ { "field": "num_vars", @@ -1083,8 +1189,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 39, - "target": 41, + "source": 47, + "target": 51, "overhead": [ { "field": "num_spins", @@ -1094,7 +1200,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 40, + "source": 49, "target": 4, "overhead": [ { @@ -1109,8 +1215,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 40, - "target": 12, + "source": 49, + "target": 15, "overhead": [ { "field": "num_vertices", @@ -1124,8 +1230,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 40, - "target": 17, + "source": 49, + "target": 20, "overhead": [ { "field": "num_clauses", @@ -1139,8 +1245,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 40, - "target": 25, + "source": 49, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -1154,8 +1260,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 40, - "target": 34, + "source": 49, + "target": 38, "overhead": [ { "field": "num_vertices", @@ -1169,8 +1275,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 41, - "target": 39, + "source": 51, + "target": 47, "overhead": [ { "field": "num_vars", @@ -1180,8 +1286,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 42, - "target": 20, + "source": 52, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -1195,8 +1301,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 42, - "target": 41, + "source": 52, + "target": 51, "overhead": [ { "field": "num_spins", @@ -1210,8 +1316,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 44, - "target": 9, + "source": 55, + "target": 11, "overhead": [ { "field": "num_vars", @@ -1223,6 +1329,17 @@ } ], "doc_path": "rules/travelingsalesman_ilp/index.html" + }, + { + "source": 55, + "target": 47, + "overhead": [ + { + "field": "num_vars", + "formula": "num_vertices^2" + } + ], + "doc_path": "rules/travelingsalesman_qubo/index.html" } ] } \ No newline at end of file diff --git a/examples/reduction_ksatisfiability_to_subsetsum.rs b/examples/reduction_ksatisfiability_to_subsetsum.rs index 92634e35a..dc1cadd92 100644 --- a/examples/reduction_ksatisfiability_to_subsetsum.rs +++ b/examples/reduction_ksatisfiability_to_subsetsum.rs @@ -114,8 +114,8 @@ pub fn run() { variant: target_variant, instance: serde_json::json!({ "num_elements": subsetsum.num_elements(), - "sizes": subsetsum.sizes(), - "target": subsetsum.target(), + "sizes": subsetsum.sizes().iter().map(ToString::to_string).collect::>(), + "target": subsetsum.target().to_string(), }), }, overhead: overhead_to_json(&overhead), diff --git a/problemreductions-cli/Cargo.toml b/problemreductions-cli/Cargo.toml index 501f7e519..4bf4a70e0 100644 --- a/problemreductions-cli/Cargo.toml +++ b/problemreductions-cli/Cargo.toml @@ -30,6 +30,7 @@ clap = { version = "4", features = ["derive"] } anyhow = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" +num-bigint = "0.4" clap_complete = "4" owo-colors = { version = "4", features = ["supports-colors"] } rmcp = { version = "0.16", features = ["server", "macros", "transport-io"], optional = true } diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 3a18e8a60..b7fbd4730 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -288,9 +288,9 @@ pub struct CreateArgs { /// Random seed for reproducibility #[arg(long)] pub seed: Option, - /// Target number to factor (for Factoring) + /// Target value (for Factoring and SubsetSum) #[arg(long)] - pub target: Option, + pub target: Option, /// Bits for first factor (for Factoring) #[arg(long)] pub m: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 97035d180..8cf415ff6 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -75,6 +75,8 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "usize" => "integer", "u64" => "integer", "i64" => "integer", + "BigUint" => "nonnegative decimal integer", + "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", "Vec" => "comma-separated integers: 3,7,1,8", "DirectedGraph" => "directed arcs: 0>1,1>2,2>0", _ => "value", @@ -416,7 +418,11 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let usage = "Usage: pred create Factoring --target 15 --m 4 --n 4"; let target = args .target + .as_deref() .ok_or_else(|| anyhow::anyhow!("Factoring requires --target\n\n{usage}"))?; + let target: u64 = target + .parse() + .context("Factoring --target must fit in u64")?; let m = args .m .ok_or_else(|| anyhow::anyhow!("Factoring requires --m\n\n{usage}"))?; @@ -470,15 +476,16 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { Usage: pred create SubsetSum --sizes 3,7,1,8,2,4 --target 11" ) })?; - let target = args.target.ok_or_else(|| { + let target = args.target.as_deref().ok_or_else(|| { anyhow::anyhow!( "SubsetSum requires --target\n\n\ Usage: pred create SubsetSum --sizes 3,7,1,8,2,4 --target 11" ) })?; - let sizes: Vec = util::parse_comma_list(sizes_str)?; + let sizes = util::parse_biguint_list(sizes_str)?; + let target = util::parse_decimal_biguint(target)?; ( - ser(SubsetSum::new(sizes, target as i64))?, + ser(SubsetSum::new(sizes, target))?, resolved_variant.clone(), ) } diff --git a/problemreductions-cli/src/util.rs b/problemreductions-cli/src/util.rs index 5452c27f7..bc63585c0 100644 --- a/problemreductions-cli/src/util.rs +++ b/problemreductions-cli/src/util.rs @@ -1,6 +1,7 @@ //! Shared utilities for CLI and MCP: parsing helpers and random generation. use anyhow::{bail, Result}; +use num_bigint::BigUint; use problemreductions::prelude::*; use problemreductions::topology::SimpleGraph; use problemreductions::variant::{K2, K3, KN}; @@ -242,6 +243,17 @@ where .collect() } +pub fn parse_decimal_biguint(s: &str) -> Result { + BigUint::parse_bytes(s.trim().as_bytes(), 10) + .ok_or_else(|| anyhow::anyhow!("Invalid decimal integer '{}'", s.trim())) +} + +pub fn parse_biguint_list(s: &str) -> Result> { + s.split(',') + .map(|value| parse_decimal_biguint(value.trim())) + .collect() +} + /// Parse edge pairs like "0-1,1-2,2-3" into Vec<(usize, usize)>. pub fn parse_edge_pairs(s: &str) -> Result> { s.split(',') diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index 85a4d7936..dff17693d 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -163,7 +163,7 @@ impl Problem for IsomorphicSpanningTree { impl SatisfactionProblem for IsomorphicSpanningTree {} crate::declare_variants! { - IsomorphicSpanningTree => "num_vertices^num_vertices", + IsomorphicSpanningTree => "factorial(num_vertices)", } #[cfg(test)] diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index 5613258de..dbd8ae509 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -269,7 +269,7 @@ where } crate::declare_variants! { - RuralPostman => "num_vertices^2 * 2^num_required_edges", + RuralPostman => "2^num_vertices * num_vertices^2", } #[cfg(test)] diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index ecb520a9c..38e315da8 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -1,29 +1,34 @@ //! Subset Sum problem implementation. //! -//! Given a set of integers and a target value, the problem asks whether any -//! subset sums to exactly the target. One of Karp's original 21 NP-complete +//! Given a set of positive integers and a target value, the problem asks whether +//! any subset sums to exactly the target. One of Karp's original 21 NP-complete //! problems (1972). +//! +//! This implementation uses arbitrary-precision integers (`BigUint`) so +//! reductions can construct large instances without fixed-width overflow. use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::traits::{Problem, SatisfactionProblem}; +use num_bigint::{BigUint, ToBigUint}; +use num_traits::Zero; use serde::{Deserialize, Serialize}; inventory::submit! { ProblemSchemaEntry { name: "SubsetSum", module_path: module_path!(), - description: "Find a subset of integers that sums to exactly a target value", + description: "Find a subset of positive integers that sums to exactly a target value", fields: &[ - FieldInfo { name: "sizes", type_name: "Vec", description: "Integer sizes s(a) for each element" }, - FieldInfo { name: "target", type_name: "i64", description: "Target sum B" }, + FieldInfo { name: "sizes", type_name: "Vec", description: "Positive integer sizes s(a) for each element" }, + FieldInfo { name: "target", type_name: "BigUint", description: "Target sum B" }, ], } } /// The Subset Sum problem. /// -/// Given a set of `n` integers and a target `B`, determine whether there exists -/// a subset whose elements sum to exactly `B`. +/// Given a set of `n` positive integers and a target `B`, determine whether +/// there exists a subset whose elements sum to exactly `B`. /// /// # Representation /// @@ -36,31 +41,60 @@ inventory::submit! { /// use problemreductions::models::misc::SubsetSum; /// use problemreductions::{Problem, Solver, BruteForce}; /// -/// let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); +/// let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); /// let solver = BruteForce::new(); /// let solution = solver.find_satisfying(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SubsetSum { - sizes: Vec, - target: i64, + #[serde(with = "decimal_biguint_vec")] + sizes: Vec, + #[serde(with = "decimal_biguint")] + target: BigUint, } impl SubsetSum { /// Create a new SubsetSum instance. - pub fn new(sizes: Vec, target: i64) -> Self { + /// + /// # Panics + /// + /// Panics if any size is not positive (must be > 0). + pub fn new(sizes: Vec, target: T) -> Self + where + S: ToBigUint, + T: ToBigUint, + { + let sizes: Vec = sizes + .into_iter() + .map(|s| s.to_biguint().expect("All sizes must be positive (> 0)")) + .collect(); + assert!( + sizes.iter().all(|s| !s.is_zero()), + "All sizes must be positive (> 0)" + ); + let target = target + .to_biguint() + .expect("SubsetSum target must be nonnegative"); + Self { sizes, target } + } + + /// Create a new SubsetSum instance without validating sizes. + /// + /// This is intended for reductions that produce SubsetSum instances + /// where positivity is guaranteed by construction. + pub(crate) fn new_unchecked(sizes: Vec, target: BigUint) -> Self { Self { sizes, target } } /// Returns the element sizes. - pub fn sizes(&self) -> &[i64] { + pub fn sizes(&self) -> &[BigUint] { &self.sizes } /// Returns the target sum. - pub fn target(&self) -> i64 { - self.target + pub fn target(&self) -> &BigUint { + &self.target } /// Returns the number of elements. @@ -88,12 +122,12 @@ impl Problem for SubsetSum { if config.iter().any(|&v| v >= 2) { return false; } - let total: i64 = config - .iter() - .enumerate() - .filter(|(_, &x)| x == 1) - .map(|(i, _)| self.sizes[i]) - .sum(); + let mut total = BigUint::zero(); + for (i, &x) in config.iter().enumerate() { + if x == 1 { + total += &self.sizes[i]; + } + } total == self.target } } @@ -104,6 +138,68 @@ crate::declare_variants! { SubsetSum => "2^(num_elements / 2)", } +mod decimal_biguint { + use super::BigUint; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + #[derive(Deserialize)] + #[serde(untagged)] + pub(super) enum Repr { + String(String), + U64(u64), + I64(i64), + } + + pub(super) fn parse_repr(value: Repr) -> Result { + match value { + Repr::String(s) => BigUint::parse_bytes(s.as_bytes(), 10) + .ok_or_else(|| E::custom(format!("invalid decimal integer: {s}"))), + Repr::U64(n) => Ok(BigUint::from(n)), + Repr::I64(n) if n >= 0 => Ok(BigUint::from(n as u64)), + Repr::I64(n) => Err(E::custom(format!("expected nonnegative integer, got {n}"))), + } + } + + pub fn serialize(value: &BigUint, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_str_radix(10)) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + parse_repr(Repr::deserialize(deserializer)?) + } +} + +mod decimal_biguint_vec { + use super::BigUint; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(values: &[BigUint], serializer: S) -> Result + where + S: Serializer, + { + let strings: Vec = values.iter().map(ToString::to_string).collect(); + strings.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let values = Vec::::deserialize(deserializer)?; + values + .into_iter() + .map(super::decimal_biguint::parse_repr::) + .collect() + } +} + #[cfg(test)] #[path = "../../unit_tests/models/misc/subset_sum.rs"] mod tests; diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 1bd3d7352..508ec077c 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -7,6 +7,9 @@ //! //! No carries occur because the maximum digit sum is at most 3 + 2 = 5 < 10. //! +//! Uses `SubsetSum` with arbitrary-precision integers so the encoding does not +//! overflow on large instances. +//! //! Reference: Karp 1972; Sipser Theorem 7.56; CLRS §34.5.5 use crate::models::formula::KSatisfiability; @@ -14,6 +17,8 @@ use crate::models::misc::SubsetSum; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::variant::K3; +use num_bigint::BigUint; +use num_traits::Zero; /// Result of reducing KSatisfiability to SubsetSum. #[derive(Debug, Clone)] @@ -50,10 +55,11 @@ impl ReductionResult for Reduction3SATToSubsetSum { /// Build a base-10 integer from a digit array (most significant first). /// /// digits[0] is the most significant digit, digits[num_digits-1] is the least. -fn digits_to_integer(digits: &[i64]) -> i64 { - let mut value: i64 = 0; +fn digits_to_integer(digits: &[u8]) -> BigUint { + let mut value = BigUint::zero(); + let ten = BigUint::from(10u8); for &d in digits { - value = value * 10 + d; + value = value * &ten + BigUint::from(d); } value } @@ -68,18 +74,13 @@ impl ReduceTo for KSatisfiability { let n = self.num_vars(); let m = self.num_clauses(); let num_digits = n + m; - assert!( - num_digits <= 18, - "3-SAT to SubsetSum reduction requires n + m <= 18 for i64 encoding \ - (got n={n}, m={m}, n+m={num_digits})" - ); let mut sizes = Vec::with_capacity(2 * n + 2 * m); // Step 1: Variable integers (2n integers) for i in 0..n { // y_i: d_i = 1, d_{n+j} = 1 if x_{i+1} appears positive in C_j - let mut y_digits = vec![0i64; num_digits]; + let mut y_digits = vec![0u8; num_digits]; y_digits[i] = 1; for (j, clause) in self.clauses().iter().enumerate() { for &lit in &clause.literals { @@ -92,7 +93,7 @@ impl ReduceTo for KSatisfiability { sizes.push(digits_to_integer(&y_digits)); // z_i: d_i = 1, d_{n+j} = 1 if ¬x_{i+1} appears in C_j - let mut z_digits = vec![0i64; num_digits]; + let mut z_digits = vec![0u8; num_digits]; z_digits[i] = 1; for (j, clause) in self.clauses().iter().enumerate() { for &lit in &clause.literals { @@ -108,18 +109,18 @@ impl ReduceTo for KSatisfiability { // Step 2: Slack integers (2m integers) for j in 0..m { // g_j: d_{n+j} = 1 - let mut g_digits = vec![0i64; num_digits]; + let mut g_digits = vec![0u8; num_digits]; g_digits[n + j] = 1; sizes.push(digits_to_integer(&g_digits)); // h_j: d_{n+j} = 2 - let mut h_digits = vec![0i64; num_digits]; + let mut h_digits = vec![0u8; num_digits]; h_digits[n + j] = 2; sizes.push(digits_to_integer(&h_digits)); } // Step 3: Target - let mut target_digits = vec![0i64; num_digits]; + let mut target_digits = vec![0u8; num_digits]; for d in target_digits.iter_mut().take(n) { *d = 1; } @@ -129,7 +130,7 @@ impl ReduceTo for KSatisfiability { let target = digits_to_integer(&target_digits); Reduction3SATToSubsetSum { - target: SubsetSum::new(sizes, target), + target: SubsetSum::new_unchecked(sizes, target), source_num_vars: n, } } diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index 6755001b3..dac5a34be 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; @@ -253,3 +253,25 @@ fn test_size_getters() { assert_eq!(problem.m(), 3); // rows assert_eq!(problem.n(), 2); // cols } + +#[test] +fn test_bmf_paper_example() { + // Paper: A=[[1,1,0],[1,1,1],[0,1,1]], k=2, exact factorization + let matrix = vec![ + vec![true, true, false], + vec![true, true, true], + vec![false, true, true], + ]; + let problem = BMF::new(matrix, 2); + // B (3x2): [[1,0],[1,1],[0,1]], C (2x3): [[1,1,0],[0,1,1]] + // Config: B row-major then C row-major + // B: b00=1,b01=0, b10=1,b11=1, b20=0,b21=1 + // C: c00=1,c01=1,c02=0, c10=0,c11=1,c12=1 + let config = vec![1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1]; + let result = Problem::evaluate(&problem, &config); + assert_eq!(result, SolutionSize::Valid(0)); // exact factorization + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(Problem::evaluate(&problem, &best), SolutionSize::Valid(0)); +} diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index 4d61d5204..7bb465789 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -176,3 +176,26 @@ fn test_cvp_inconsistent_dimensions() { let bounds = vec![VarBounds::bounded(0, 1), VarBounds::bounded(0, 1)]; ClosestVectorProblem::new(basis, target, bounds); } + +#[test] +fn test_cvp_paper_example() { + // Paper: basis (2,0),(1,2), target (2.8,1.5), closest (3,2) at x=(1,1) + let basis = vec![vec![2, 0], vec![1, 2]]; + let target = vec![2.8, 1.5]; + let bounds = vec![VarBounds::bounded(-2, 4), VarBounds::bounded(-2, 4)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + // x=(1,1): Bx = 2*1+1*1=3, 0*1+2*1=2 -> point (3,2) + // distance = sqrt((2.8-3)^2 + (1.5-2)^2) = sqrt(0.04+0.25) = sqrt(0.29) + // config offset: x_i - lower = 1 - (-2) = 3 + let config = vec![3, 3]; // maps to x=(1,1) + let result = Problem::evaluate(&cvp, &config); + assert!(result.is_valid()); + let dist = result.unwrap(); + assert!((dist - 0.29_f64.sqrt()).abs() < 1e-10); + + let solver = BruteForce::new(); + let best = solver.find_best(&cvp).unwrap(); + let best_dist = Problem::evaluate(&cvp, &best).unwrap(); + assert!((best_dist - 0.29_f64.sqrt()).abs() < 1e-10); +} diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index b7bb0e976..91f60cc27 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -425,3 +425,33 @@ fn test_ilp_i32_dims() { let ilp = ILP::::new(3, vec![], vec![], ObjectiveSense::Minimize); assert_eq!(ilp.dims(), vec![(i32::MAX as usize) + 1; 3]); } + +#[test] +fn test_ilp_paper_example() { + // Paper: minimize -5x₁ - 6x₂ + // s.t. x₁ + x₂ ≤ 5, 4x₁ + 7x₂ ≤ 28, x₁, x₂ ≥ 0, x ∈ Z² + // Optimal: x* = (3, 2), objective = -27 + let ilp = ILP::::new( + 2, + vec![ + LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 5.0), + LinearConstraint::le(vec![(0, 4.0), (1, 7.0)], 28.0), + ], + vec![(0, -5.0), (1, -6.0)], + ObjectiveSense::Minimize, + ); + + // Verify optimal solution x* = (3, 2) → config [3, 2] + let result = Problem::evaluate(&ilp, &[3, 2]); + assert_eq!(result, SolutionSize::Valid(-27.0)); + + // Verify feasibility: 3+2=5≤5, 4*3+7*2=26≤28 + assert!(ilp.is_feasible(&[3, 2])); + + // Verify infeasible point: 4+4=8>5 + assert!(!ilp.is_feasible(&[4, 4])); + + // Verify suboptimal feasible point: -5*0 - 6*4 = -24 > -27 + let result2 = Problem::evaluate(&ilp, &[0, 4]); + assert_eq!(result2, SolutionSize::Valid(-24.0)); +} diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index c08827b2c..550810cf5 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; include!("../../jl_helpers.rs"); @@ -109,3 +109,18 @@ fn test_jl_parity_evaluation() { assert_eq!(rust_best, jl_best, "QUBO best solutions mismatch"); } } + +#[test] +fn test_qubo_paper_example() { + // Paper: Q=[[-1,2,0],[0,-1,2],[0,0,-1]], min=-2 at (1,0,1) + let problem = QUBO::from_matrix(vec![ + vec![-1.0, 2.0, 0.0], + vec![0.0, -1.0, 2.0], + vec![0.0, 0.0, -1.0], + ]); + assert_eq!(Problem::evaluate(&problem, &[1, 0, 1]), SolutionSize::Valid(-2.0)); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(Problem::evaluate(&problem, &best), SolutionSize::Valid(-2.0)); +} diff --git a/src/unit_tests/models/formula/circuit.rs b/src/unit_tests/models/formula/circuit.rs index 0907980c2..81a9e1899 100644 --- a/src/unit_tests/models/formula/circuit.rs +++ b/src/unit_tests/models/formula/circuit.rs @@ -261,3 +261,35 @@ fn test_size_getters() { let problem = CircuitSAT::new(circuit); assert_eq!(problem.num_variables(), 3); } + +#[test] +fn test_circuit_sat_paper_example() { + // Paper: C(x1, x2) = (x1 AND x2) XOR (x1 OR x2) + let circuit = Circuit::new(vec![ + Assignment::new( + vec!["a".to_string()], + BooleanExpr::and(vec![BooleanExpr::var("x1"), BooleanExpr::var("x2")]), + ), + Assignment::new( + vec!["b".to_string()], + BooleanExpr::or(vec![BooleanExpr::var("x1"), BooleanExpr::var("x2")]), + ), + Assignment::new( + vec!["c".to_string()], + BooleanExpr::xor(vec![BooleanExpr::var("a"), BooleanExpr::var("b")]), + ), + ]); + let problem = CircuitSAT::new(circuit); + + // Variables sorted: a, b, c, x1, x2 + // Paper satisfying inputs (output c=1): (x1=0,x2=1) and (x1=1,x2=0) + // (x1=0,x2=1): a=0, b=1, c=1 → config [0, 1, 1, 0, 1] + assert!(problem.evaluate(&[0, 1, 1, 0, 1])); + // (x1=1,x2=0): a=0, b=1, c=1 → config [0, 1, 1, 1, 0] + assert!(problem.evaluate(&[0, 1, 1, 1, 0])); + + // All 4 consistent configs are satisfying (CircuitSAT checks consistency) + let solver = BruteForce::new(); + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 4); +} diff --git a/src/unit_tests/models/formula/ksat.rs b/src/unit_tests/models/formula/ksat.rs index 94f6b306c..420766fc4 100644 --- a/src/unit_tests/models/formula/ksat.rs +++ b/src/unit_tests/models/formula/ksat.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::Problem; use crate::variant::{K2, K3, KN}; include!("../../jl_helpers.rs"); @@ -226,3 +226,18 @@ fn test_size_getters() { assert_eq!(problem.num_clauses(), 2); assert_eq!(problem.num_literals(), 6); // 3 + 3 } + +#[test] +fn test_ksat_paper_example() { + // Paper: 3-SAT, (x1∨x2∨x3)∧(¬x1∨¬x2∨x3)∧(x1∨¬x2∨¬x3), assignment (1,0,1) + let problem = KSatisfiability::::new(3, vec![ + CNFClause::new(vec![1, 2, 3]), + CNFClause::new(vec![-1, -2, 3]), + CNFClause::new(vec![1, -2, -3]), + ]); + assert!(problem.evaluate(&[1, 0, 1])); + + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); +} diff --git a/src/unit_tests/models/formula/sat.rs b/src/unit_tests/models/formula/sat.rs index cd346f859..d2f61cdc0 100644 --- a/src/unit_tests/models/formula/sat.rs +++ b/src/unit_tests/models/formula/sat.rs @@ -207,3 +207,19 @@ fn test_is_valid_solution() { // Invalid: x1=T, x2=F, x3=F → (T) AND (F) = F assert!(!problem.is_valid_solution(&[1, 0, 0])); } + +#[test] +fn test_sat_paper_example() { + // Paper: (x1∨x2)∧(¬x1∨x3)∧(¬x2∨¬x3), assignment (1,0,1) + let problem = Satisfiability::new(3, vec![ + CNFClause::new(vec![1, 2]), + CNFClause::new(vec![-1, 3]), + CNFClause::new(vec![-2, -3]), + ]); + // (1,0,1) → x1=T, x2=F, x3=T + assert!(problem.evaluate(&[1, 0, 1])); + + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); +} diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index d27f0cf89..1d48479af 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::BipartiteGraph; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; @@ -253,3 +253,23 @@ fn test_size_getters() { assert_eq!(problem.k(), 1); assert_eq!(problem.rank(), 1); } + +#[test] +fn test_biclique_paper_example() { + // Paper: L={ℓ_1,ℓ_2}, R={r_1,r_2,r_3}, 4 edges, k=2, total size=6 + let graph = BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]); + let problem = BicliqueCover::new(graph, 2); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 4); + + // Biclique 0: {ℓ_1}, {r_1,r_2}; Biclique 1: {ℓ_2}, {r_2,r_3} + let config = vec![1, 0, 0, 1, 1, 0, 1, 1, 0, 1]; + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 6); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + let best_size = problem.evaluate(&best).unwrap(); + assert!(best_size <= 6); +} diff --git a/src/unit_tests/models/graph/hamiltonian_path.rs b/src/unit_tests/models/graph/hamiltonian_path.rs index fcb2d167a..493769055 100644 --- a/src/unit_tests/models/graph/hamiltonian_path.rs +++ b/src/unit_tests/models/graph/hamiltonian_path.rs @@ -127,6 +127,37 @@ fn test_size_getters() { assert_eq!(problem.num_edges(), 4); } +#[test] +fn test_hamiltonianpath_paper_example() { + use crate::traits::Problem; + + // Paper/issue #217: 6 vertices, 8 edges + let problem = HamiltonianPath::new(SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (3, 4), + (3, 5), + (4, 2), + (5, 1), + ], + )); + + // Hamiltonian path: 0→2→4→3→1→5 + assert!(problem.evaluate(&[0, 2, 4, 3, 1, 5])); + + // Verify with brute force + let solver = BruteForce::new(); + let all = solver.find_all_satisfying(&problem); + assert!(!all.is_empty()); + for sol in &all { + assert!(problem.evaluate(sol)); + } +} + #[test] fn test_single_vertex() { use crate::traits::Problem; diff --git a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs index 8b13ac344..3a871f223 100644 --- a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs +++ b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs @@ -150,6 +150,24 @@ fn test_isomorphicspanningtree_caterpillar_example() { assert!(problem.evaluate(&[0, 1, 2, 3, 6, 4, 5])); } +#[test] +fn test_isomorphicspanningtree_paper_example() { + // Paper example: G = K4, T = star S3 (center 0, leaves {1, 2, 3}) + // Any bijection works since K4 has all edges. + // Identity mapping π(i) = i embeds star edges {(0,1),(0,2),(0,3)} into K4. + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); // star S3 + let problem = IsomorphicSpanningTree::new(graph, tree); + + // Identity mapping: π = [0, 1, 2, 3] + assert!(problem.evaluate(&[0, 1, 2, 3])); + + // All 4! = 24 permutations should work since K4 has every edge + let solver = BruteForce::new(); + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 24); +} + #[test] fn test_isomorphicspanningtree_variant() { assert!(IsomorphicSpanningTree::variant().is_empty()); diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index df23bf8c4..a2ab6e79a 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::variant::{K1, K2, K3, K4}; include!("../../jl_helpers.rs"); @@ -195,3 +195,19 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } + +#[test] +fn test_kcoloring_paper_example() { + use crate::traits::Problem; + // Paper: house graph, k=3, proper coloring [0,1,1,0,2], chi(G)=3 + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem = KColoring::::new(graph); + let config = vec![0, 1, 1, 0, 2]; + assert!(problem.evaluate(&config)); + + // Verify not 2-colorable (triangle v_2,v_3,v_4) + let graph2 = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem2 = KColoring::::new(graph2); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem2).is_none()); +} diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index 6daa8c20f..345859e54 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; include!("../../jl_helpers.rs"); @@ -147,3 +147,19 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } + +#[test] +fn test_maxcut_paper_example() { + use crate::traits::Problem; + // Paper: house graph, S = {v_0, v_3}, cut = 5 + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem = MaxCut::<_, i32>::unweighted(graph); + let config = vec![1, 0, 0, 1, 0]; // S = {v_0, v_3} + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 5); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 5); +} diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index 51ef90b09..cff5e0cc0 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; include!("../../jl_helpers.rs"); @@ -183,3 +183,28 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } + +#[test] +fn test_maximal_is_paper_example() { + use crate::traits::Problem; + // Paper: path P5, maximal IS {v_1, v_3} (weight 2), {v_0, v_2, v_4} (weight 3) + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let problem = MaximalIS::new(graph, vec![1i32; 5]); + + // {v_1, v_3} is maximal (can't add v_0: adj to v_1, can't add v_2: adj to both, can't add v_4: adj to v_3) + let config1 = vec![0, 1, 0, 1, 0]; + let result1 = problem.evaluate(&config1); + assert!(result1.is_valid()); + assert_eq!(result1.unwrap(), 2); + + // {v_0, v_2, v_4} is also maximal, weight 3 (maximum weight maximal IS) + let config2 = vec![1, 0, 1, 0, 1]; + let result2 = problem.evaluate(&config2); + assert!(result2.is_valid()); + assert_eq!(result2.unwrap(), 3); + + // Verify optimal weight is 3 + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 3); +} diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 4723834ee..1004037a8 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::types::SolutionSize; @@ -291,3 +291,19 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } + +#[test] +fn test_clique_paper_example() { + use crate::traits::Problem; + // Paper: house graph, max clique K = {v_2, v_3, v_4}, omega(G) = 3 + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem = MaximumClique::new(graph, vec![1i32; 5]); + let config = vec![0, 0, 1, 1, 1]; // {v_2, v_3, v_4} + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 3); +} diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index a4964112a..5c0f2352d 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::traits::{OptimizationProblem, Problem}; use crate::types::Direction; @@ -182,3 +182,39 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } + +#[test] +fn test_mis_paper_example() { + // Paper: Petersen graph, MIS = {v_1, v_3, v_5, v_9}, weight = 4 + let graph = SimpleGraph::new( + 10, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 0), // outer + (5, 7), + (7, 9), + (9, 6), + (6, 8), + (8, 5), // inner + (0, 5), + (1, 6), + (2, 7), + (3, 8), + (4, 9), // spokes + ], + ); + let problem = MaximumIndependentSet::new(graph, vec![1i32; 10]); + // MIS = {1,3,5,9} -> config + let config = vec![0, 1, 0, 1, 0, 1, 0, 0, 0, 1]; + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 4); + + // Verify this is optimal + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 4); +} diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index 32068e8f0..d74eea97f 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; @@ -176,3 +176,20 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } + +#[test] +fn test_matching_paper_example() { + // Paper: house graph, M = {(v_0,v_1), (v_2,v_4)}, weight = 2 + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem = MaximumMatching::<_, i32>::unit_weights(graph); + // Edges: 0=(0,1), 1=(0,2), 2=(1,3), 3=(2,3), 4=(2,4), 5=(3,4) + // Select edges 0 and 4 + let config = vec![1, 0, 0, 0, 1, 0]; + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 2); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 2); +} diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index 68dd26d2c..7e5684bef 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::traits::{OptimizationProblem, Problem}; use crate::types::Direction; @@ -177,3 +177,18 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } + +#[test] +fn test_mds_paper_example() { + // Paper: house graph, DS = {v_2, v_3}, weight = 2, gamma(G) = 2 + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem = MinimumDominatingSet::new(graph, vec![1i32; 5]); + let config = vec![0, 0, 1, 1, 0]; // {v_2, v_3} + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 2); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 2); +} diff --git a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs index 7e6ee138d..353c585b6 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -180,3 +180,34 @@ fn test_is_feedback_vertex_set_helper() { let empty = [false; 9]; assert!(!is_feedback_vertex_set(&graph, &empty)); } + +#[test] +fn test_minimum_feedback_vertex_set_paper_example() { + // Paper: 5 vertices, 7 arcs, two overlapping cycles: + // C_1 = v_0→v_1→v_2→v_0, C_2 = v_0→v_3→v_4→v_1 + // 7th arc: (4,2) — removing v_0 leaves DAG with topo order (v_3, v_4, v_1, v_2) + // FVS = {v_0}, weight = 1 + let graph = DirectedGraph::new( + 5, + vec![(0, 1), (1, 2), (2, 0), (0, 3), (3, 4), (4, 1), (4, 2)], + ); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 5]); + + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_arcs(), 7); + + // {v_0} is a valid FVS with weight 1 + let config = vec![1, 0, 0, 0, 0]; + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 1); + + // Removing v_1 alone leaves cycle v_0→v_3→v_4→...→v_2→v_0 (through arc (2,0)) + let config_v1 = vec![0, 1, 0, 0, 0]; + assert!(!problem.evaluate(&config_v1).is_valid()); + + // Verify optimal FVS weight is 1 + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 1); +} diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index f6055d1d3..b6aa0aac0 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -199,6 +199,37 @@ fn test_min_sum_multicenter_k_too_large() { MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 4); } +#[test] +fn test_min_sum_multicenter_paper_example() { + // Paper/issue #399: 7 vertices, 8 edges, unit weights, K=2 + let graph = SimpleGraph::new( + 7, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (0, 6), + (2, 5), + ], + ); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 7], vec![1i32; 8], 2); + + // Optimal: centers at {2, 5}, config [0,0,1,0,0,1,0] + // Distances: d(0)=2, d(1)=1, d(2)=0, d(3)=1, d(4)=1, d(5)=0, d(6)=1 + // Total = 6 + let result = problem.evaluate(&[0, 0, 1, 0, 0, 1, 0]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 6); + + // Verify optimality with brute force + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 6); +} + #[test] fn test_min_sum_multicenter_dims() { let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index 6934a3574..ba2a36648 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::traits::{OptimizationProblem, Problem}; use crate::types::Direction; @@ -161,3 +161,18 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } + +#[test] +fn test_mvc_paper_example() { + // Paper: house graph, VC = {v_0, v_3, v_4}, weight = 3 + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let problem = MinimumVertexCover::new(graph, vec![1i32; 5]); + let config = vec![1, 0, 0, 1, 1]; // {v_0, v_3, v_4} + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 3); +} diff --git a/src/unit_tests/models/graph/partition_into_triangles.rs b/src/unit_tests/models/graph/partition_into_triangles.rs index 86fc7d71c..47b777e22 100644 --- a/src/unit_tests/models/graph/partition_into_triangles.rs +++ b/src/unit_tests/models/graph/partition_into_triangles.rs @@ -125,3 +125,20 @@ fn test_partitionintotriangles_size_getters() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 6); } + +#[test] +fn test_partitionintotriangles_paper_example() { + use crate::traits::Problem; + // Paper: 6 vertices, two triangles + cross-edge (0,3) + let graph = SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (0, 3)], + ); + let problem = PartitionIntoTriangles::new(graph); + // Valid partition: {0,1,2} in group 0, {3,4,5} in group 1 + assert!(problem.evaluate(&[0, 0, 0, 1, 1, 1])); + + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); +} diff --git a/src/unit_tests/models/graph/spin_glass.rs b/src/unit_tests/models/graph/spin_glass.rs index 7018a7473..ae05d127f 100644 --- a/src/unit_tests/models/graph/spin_glass.rs +++ b/src/unit_tests/models/graph/spin_glass.rs @@ -147,3 +147,32 @@ fn test_size_getters() { assert_eq!(problem.num_spins(), 3); assert_eq!(problem.num_interactions(), 2); } + +#[test] +fn test_spinglass_paper_example() { + // Paper: 5 spins on triangular lattice, antiferromagnetic J=-1 (paper convention) + // Code H = Σ J*s*s vs paper H = -Σ J*s*s, so J_code = -J_paper = 1 + // 7 edges on triangular lattice + let problem = SpinGlass::::without_fields( + 5, + vec![ + ((0, 1), 1), + ((1, 2), 1), + ((3, 4), 1), + ((0, 3), 1), + ((1, 3), 1), + ((1, 4), 1), + ((2, 4), 1), + ], + ); + // Ground state: s = (+1,-1,+1,+1,-1) → config x = (1,0,1,1,0) + // Energy = -3 (5 satisfied antiparallel, 2 frustrated parallel edges) + let result = problem.evaluate(&[1, 0, 1, 1, 0]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), -3); + + // Verify this is optimal + let all_best = BruteForce::new().find_all_best(&problem); + assert!(!all_best.is_empty()); + assert_eq!(problem.evaluate(&all_best[0]).unwrap(), -3); +} diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index f3748d0b6..256729509 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::topology::SimpleGraph; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; @@ -245,3 +245,22 @@ fn test_size_getters() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 3); } + +#[test] +fn test_tsp_paper_example() { + // Paper: K4, weights w(0,1)=1, w(0,2)=3, w(0,3)=2, w(1,2)=2, w(1,3)=3, w(2,3)=1 + // Optimal tour: v0→v1→v2→v3→v0, cost = 1+2+1+2 = 6 + let problem = TravelingSalesman::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + vec![1, 3, 2, 2, 3, 1], + ); + // Edges: 0=(0,1), 1=(0,2), 2=(0,3), 3=(1,2), 4=(1,3), 5=(2,3) + // Tour uses edges 0, 2, 3, 5 + let config = vec![1, 0, 1, 1, 0, 1]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(6)); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), SolutionSize::Valid(6)); +} diff --git a/src/unit_tests/models/misc/factoring.rs b/src/unit_tests/models/misc/factoring.rs index 8cfbf47ce..d2f8aa0d6 100644 --- a/src/unit_tests/models/misc/factoring.rs +++ b/src/unit_tests/models/misc/factoring.rs @@ -113,3 +113,17 @@ fn test_size_getters() { assert_eq!(problem.num_bits_first(), 3); assert_eq!(problem.num_bits_second(), 3); } + +#[test] +fn test_factoring_paper_example() { + // Paper: N=15, m=2 bits, n=3 bits, p=3, q=5 + let problem = Factoring::new(2, 3, 15); + assert_eq!(problem.num_variables(), 5); + + // p=3 -> bits [1,1], q=5 -> bits [1,0,1] + let config = vec![1, 1, 1, 0, 1]; + let (a, b) = problem.read_factors(&config); + assert_eq!(a, 3); + assert_eq!(b, 5); + assert!(problem.is_valid_solution(&config)); +} diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index b239b7473..87c0f1607 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::{OptimizationProblem, Problem}; use crate::types::Direction; include!("../../jl_helpers.rs"); @@ -137,3 +137,17 @@ fn test_size_getters() { assert_eq!(problem.num_sequence(), 4); assert_eq!(problem.num_cars(), 2); } + +#[test] +fn test_paintshop_paper_example() { + // Paper: sequence (A,B,A,C,B,C), optimal 2 color changes + let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]); + assert_eq!(problem.num_cars(), 3); + + // Car order: A=0, B=1, C=2 (sorted) + // Config [0, 0, 1]: A first=0, B first=0, C first=1 + // Coloring: A(0), B(0), A(1), C(1), B(1), C(0) -> [0,0,1,1,1,0] -> 2 switches + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 2); +} diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 00fe116e7..0725276bc 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -103,6 +103,25 @@ fn test_shortestcommonsupersequence_single_string() { assert!(!problem.evaluate(&[2, 1, 0])); } +#[test] +fn test_shortestcommonsupersequence_paper_example() { + // Paper: Σ = {a, b, c}, R = {"abc", "bac"}, supersequence "babc" (length 4) + // Mapping: a=0, b=1, c=2 + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]], 4); + // "babc" = [1, 0, 1, 2] + // "abc"=[0,1,2] embeds at positions (1,2,3), "bac"=[1,0,2] at positions (0,1,3) + assert!(problem.evaluate(&[1, 0, 1, 2])); + + // Verify a solution exists with brute force + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_some()); + + // Bound 3 is too short: LCS("abc","bac")="ac" (len 2), so SCS ≥ 3+3-2 = 4 + let tight = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]], 3); + let solver2 = BruteForce::new(); + assert!(solver2.find_satisfying(&tight).is_none()); +} + #[test] fn test_shortestcommonsupersequence_serialization() { let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]], 5); diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index 78dbb2747..67fb79764 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -1,13 +1,22 @@ use super::*; use crate::solvers::{BruteForce, Solver}; use crate::traits::Problem; +use num_bigint::BigUint; + +fn bu(n: u32) -> BigUint { + BigUint::from(n) +} + +fn buv(values: &[u32]) -> Vec { + values.iter().copied().map(BigUint::from).collect() +} #[test] fn test_subsetsum_basic() { - let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); assert_eq!(problem.num_elements(), 6); - assert_eq!(problem.sizes(), &[3, 7, 1, 8, 2, 4]); - assert_eq!(problem.target(), 11); + assert_eq!(problem.sizes(), buv(&[3, 7, 1, 8, 2, 4]).as_slice()); + assert_eq!(problem.target(), &bu(11)); assert_eq!(problem.dims(), vec![2; 6]); assert_eq!(::NAME, "SubsetSum"); assert_eq!(::variant(), vec![]); @@ -15,7 +24,7 @@ fn test_subsetsum_basic() { #[test] fn test_subsetsum_evaluate_satisfying() { - let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); // {3, 8} = 11 assert!(problem.evaluate(&[1, 0, 0, 1, 0, 0])); // {7, 4} = 11 @@ -24,7 +33,7 @@ fn test_subsetsum_evaluate_satisfying() { #[test] fn test_subsetsum_evaluate_unsatisfying() { - let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); // {3, 7} = 10 ≠ 11 assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); // empty = 0 ≠ 11 @@ -35,21 +44,21 @@ fn test_subsetsum_evaluate_unsatisfying() { #[test] fn test_subsetsum_evaluate_wrong_config_length() { - let problem = SubsetSum::new(vec![3, 7, 1], 10); + let problem = SubsetSum::new(vec![3u32, 7, 1], 10u32); assert!(!problem.evaluate(&[1, 0])); assert!(!problem.evaluate(&[1, 0, 0, 0])); } #[test] fn test_subsetsum_evaluate_invalid_variable_value() { - let problem = SubsetSum::new(vec![3, 7], 10); + let problem = SubsetSum::new(vec![3u32, 7], 10u32); assert!(!problem.evaluate(&[2, 0])); } #[test] fn test_subsetsum_empty_instance() { // Empty set, target 0: empty subset satisfies - let problem = SubsetSum::new(vec![], 0); + let problem = SubsetSum::new_unchecked(vec![], bu(0)); assert_eq!(problem.num_elements(), 0); assert_eq!(problem.dims(), Vec::::new()); assert!(problem.evaluate(&[])); @@ -58,13 +67,13 @@ fn test_subsetsum_empty_instance() { #[test] fn test_subsetsum_empty_instance_nonzero_target() { // Empty set, target 5: impossible - let problem = SubsetSum::new(vec![], 5); + let problem = SubsetSum::new_unchecked(vec![], bu(5)); assert!(!problem.evaluate(&[])); } #[test] fn test_subsetsum_brute_force() { - let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let solver = BruteForce::new(); let solution = solver .find_satisfying(&problem) @@ -74,7 +83,7 @@ fn test_subsetsum_brute_force() { #[test] fn test_subsetsum_brute_force_all() { - let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let solver = BruteForce::new(); let solutions = solver.find_all_satisfying(&problem); assert!(!solutions.is_empty()); @@ -86,7 +95,7 @@ fn test_subsetsum_brute_force_all() { #[test] fn test_subsetsum_unsatisfiable() { // Target 100 is unreachable - let problem = SubsetSum::new(vec![1, 2, 3], 100); + let problem = SubsetSum::new(vec![1u32, 2, 3], 100u32); let solver = BruteForce::new(); let solution = solver.find_satisfying(&problem); assert!(solution.is_none()); @@ -94,16 +103,34 @@ fn test_subsetsum_unsatisfiable() { #[test] fn test_subsetsum_serialization() { - let problem = SubsetSum::new(vec![3, 7, 1, 8, 2, 4], 11); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let json = serde_json::to_value(&problem).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "sizes": ["3", "7", "1", "8", "2", "4"], + "target": "11", + }) + ); let restored: SubsetSum = serde_json::from_value(json).unwrap(); assert_eq!(restored.sizes(), problem.sizes()); assert_eq!(restored.target(), problem.target()); } +#[test] +fn test_subsetsum_deserialization_legacy_numeric_json() { + let restored: SubsetSum = serde_json::from_value(serde_json::json!({ + "sizes": [3, 7, 1, 8, 2, 4], + "target": 11, + })) + .unwrap(); + assert_eq!(restored.sizes(), buv(&[3, 7, 1, 8, 2, 4]).as_slice()); + assert_eq!(restored.target(), &bu(11)); +} + #[test] fn test_subsetsum_single_element() { - let problem = SubsetSum::new(vec![5], 5); + let problem = SubsetSum::new(vec![5u32], 5u32); assert!(problem.evaluate(&[1])); assert!(!problem.evaluate(&[0])); } @@ -111,14 +138,33 @@ fn test_subsetsum_single_element() { #[test] fn test_subsetsum_all_selected() { // Target equals sum of all elements - let problem = SubsetSum::new(vec![1, 2, 3, 4], 10); + let problem = SubsetSum::new(vec![1u32, 2, 3, 4], 10u32); assert!(problem.evaluate(&[1, 1, 1, 1])); // 1+2+3+4 = 10 } #[test] fn test_subsetsum_target_zero() { // Target 0 with non-empty set: only empty subset works - let problem = SubsetSum::new(vec![1, 2, 3], 0); + let problem = SubsetSum::new_unchecked(buv(&[1, 2, 3]), bu(0)); assert!(problem.evaluate(&[0, 0, 0])); // empty subset sums to 0 assert!(!problem.evaluate(&[1, 0, 0])); // 1 != 0 } + +#[test] +#[should_panic(expected = "positive")] +fn test_subsetsum_negative_sizes_panic() { + SubsetSum::new(vec![-1i64, 2, 3], 4u32); +} + +#[test] +#[should_panic(expected = "positive")] +fn test_subsetsum_zero_size_panic() { + SubsetSum::new(vec![0i64, 2, 3], 4u32); +} + +#[test] +fn test_subsetsum_large_integer_input() { + let problem = SubsetSum::new(vec![3i128, 7, 1, 8, 2, 4], 11i128); + assert!(problem.evaluate(&[1, 0, 0, 1, 0, 0])); // 3 + 8 = 11 + assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); // 3 + 7 = 10 +} diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 7ede648c4..4025af019 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; include!("../../jl_helpers.rs"); @@ -160,3 +160,19 @@ fn test_universe_size_empty() { let problem = MaximumSetPacking::::new(vec![]); assert_eq!(problem.universe_size(), 0); } + +#[test] +fn test_setpacking_paper_example() { + // Paper: U={0..5}, sets {0,1},{1,2},{2,3},{3,4}, max packing {S_0,S_2} + let problem = MaximumSetPacking::::new(vec![ + vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 4], + ]); + let config = vec![1, 0, 1, 0]; // {S_0, S_2} + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 2); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 2); +} diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index c04aac0b0..7347cfaf4 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::BruteForce; +use crate::solvers::{BruteForce, Solver}; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; include!("../../jl_helpers.rs"); @@ -116,3 +116,19 @@ fn test_is_valid_solution() { // Invalid: only set 1 ({1,2}) doesn't cover 0 and 3 assert!(!problem.is_valid_solution(&[0, 1, 0])); } + +#[test] +fn test_setcovering_paper_example() { + // Paper: U=5, sets {0,1,2},{1,3},{2,3,4}, min cover {S_0,S_2}, weight=2 + let problem = MinimumSetCovering::::new(5, vec![ + vec![0, 1, 2], vec![1, 3], vec![2, 3, 4], + ]); + let config = vec![1, 0, 1]; // {S_0, S_2} covers all of {0,1,2,3,4} + let result = problem.evaluate(&config); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 2); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + assert_eq!(problem.evaluate(&best).unwrap(), 2); +} diff --git a/src/unit_tests/rules/ksatisfiability_subsetsum.rs b/src/unit_tests/rules/ksatisfiability_subsetsum.rs index 42f1fceaa..fddf38c34 100644 --- a/src/unit_tests/rules/ksatisfiability_subsetsum.rs +++ b/src/unit_tests/rules/ksatisfiability_subsetsum.rs @@ -3,6 +3,7 @@ use crate::models::formula::CNFClause; use crate::solvers::{BruteForce, Solver}; use crate::traits::Problem; use crate::variant::K3; +use num_bigint::BigUint; #[test] fn test_ksatisfiability_to_subsetsum_closed_loop() { @@ -21,7 +22,7 @@ fn test_ksatisfiability_to_subsetsum_closed_loop() { assert_eq!(target.num_elements(), 10); // Verify target value: 11144 - assert_eq!(target.target(), 11144); + assert_eq!(target.target(), &BigUint::from(11144u32)); let solver = BruteForce::new(); let solutions = solver.find_all_satisfying(target); @@ -97,16 +98,16 @@ fn test_ksatisfiability_to_subsetsum_structure() { // From the issue: // y1=10010, z1=10001, y2=01010, z2=01001, y3=00111, z3=00100 // g1=00010, h1=00020, g2=00001, h2=00002 - assert_eq!(sizes[0], 10010); // y1 - assert_eq!(sizes[1], 10001); // z1 - assert_eq!(sizes[2], 1010); // y2 (leading zero dropped) - assert_eq!(sizes[3], 1001); // z2 - assert_eq!(sizes[4], 111); // y3 - assert_eq!(sizes[5], 100); // z3 - assert_eq!(sizes[6], 10); // g1 - assert_eq!(sizes[7], 20); // h1 - assert_eq!(sizes[8], 1); // g2 - assert_eq!(sizes[9], 2); // h2 + assert_eq!(sizes[0], BigUint::from(10010u32)); // y1 + assert_eq!(sizes[1], BigUint::from(10001u32)); // z1 + assert_eq!(sizes[2], BigUint::from(1010u32)); // y2 (leading zero dropped) + assert_eq!(sizes[3], BigUint::from(1001u32)); // z2 + assert_eq!(sizes[4], BigUint::from(111u32)); // y3 + assert_eq!(sizes[5], BigUint::from(100u32)); // z3 + assert_eq!(sizes[6], BigUint::from(10u32)); // g1 + assert_eq!(sizes[7], BigUint::from(20u32)); // h1 + assert_eq!(sizes[8], BigUint::from(1u32)); // g2 + assert_eq!(sizes[9], BigUint::from(2u32)); // h2 } #[test]