Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ Required tests:
- `test_<name>_direction` -- verify optimization direction (if optimization problem)
- `test_<name>_serialization` -- round-trip serde test (optional but recommended)
- `test_<name>_solver` -- verify brute-force solver finds correct solutions
- `test_<name>_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_<name>_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.

Expand Down Expand Up @@ -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_<name>_paper_example` that verifies the exact instance, solution, and solution count shown in the paper |
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"target": {
"problem": "MaximumClique",
"variant": {
"weight": "i32",
"graph": "SimpleGraph"
"graph": "SimpleGraph",
"weight": "i32"
},
"instance": {
"edges": [
Expand Down
116 changes: 116 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SubgraphIsomorphism": [Subgraph Isomorphism],
"PartitionIntoTriangles": [Partition Into Triangles],
"FlowShopScheduling": [Flow Shop Scheduling],
)

Expand Down Expand Up @@ -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$.],
) <fig:hamiltonian-path>
]
#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$?
Expand Down Expand Up @@ -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.],
) <fig:minimum-sum-multicenter>
]

== Set Problems
Expand Down Expand Up @@ -1019,6 +1071,37 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa
) <fig:biclique-cover>
]

#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.],
) <fig:partition-triangles>
]

#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$.
][
Expand Down Expand Up @@ -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.
][
Expand Down
86 changes: 86 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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},
Expand Down
45 changes: 41 additions & 4 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u64>>",
"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",
Expand Down Expand Up @@ -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<W>",
"description": "Arc weights w: A -> R"
}
]
},
{
"name": "MinimumFeedbackVertexSet",
"description": "Find minimum weight feedback vertex set in a directed graph",
Expand Down Expand Up @@ -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<i64>",
"description": "Integer sizes s(a) for each element"
"type_name": "Vec<BigUint>",
"description": "Positive integer sizes s(a) for each element"
},
{
"name": "target",
"type_name": "i64",
"type_name": "BigUint",
"description": "Target sum B"
}
]
Expand Down
Loading
Loading