diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 8298a429c..6202fbde5 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -31,6 +31,7 @@ "MaxCut": [Max-Cut], "GraphPartitioning": [Graph Partitioning], "HamiltonianPath": [Hamiltonian Path], + "IsomorphicSpanningTree": [Isomorphic Spanning Tree], "KColoring": [$k$-Coloring], "MinimumDominatingSet": [Minimum Dominating Set], "MaximumMatching": [Maximum Matching], @@ -59,9 +60,11 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "ShortestCommonSupersequence": [Shortest Common Supersequence], "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], "SubsetSum": [Subset Sum], + "FlowShopScheduling": [Flow Shop Scheduling], ) // Definition label: "def:" — each definition block must have a matching label @@ -449,6 +452,55 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co Variables: $n = |V|$ values forming a permutation. Position $i$ holds the vertex visited at step $i$. A configuration is satisfying when it forms a valid permutation of all vertices and consecutive vertices are adjacent in $G$. ] +#problem-def("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$? +][ + A classical NP-complete problem listed as ND8 in Garey & Johnson @garey1979. The Isomorphic Spanning Tree problem strictly generalizes Hamiltonian Path: a graph $G$ has a Hamiltonian path if and only if $G$ contains a spanning tree isomorphic to the path $P_n$. The problem remains NP-complete even when $T$ is restricted to trees of bounded degree @papadimitriou1982. + + Brute-force enumeration of all bijections $pi: V_T -> V$ and checking each against the edge set of $G$ runs in $O(n! dot n)$ time. No substantially faster exact algorithm is known for general instances. + + Variables: $n = |V|$ values forming a permutation. Position $i$ holds the graph vertex that tree vertex $i$ maps to under $pi$. A configuration is satisfying when it forms a valid permutation and every tree edge maps to a graph edge. + + *Example.* Consider $G = K_4$ (the complete graph on 4 vertices) and $T$ the star $S_3$ with center $0$ and leaves ${1, 2, 3}$. Since $K_4$ contains all possible edges, any bijection $pi$ maps the star's edges to edges of $G$. For instance, the identity mapping $pi(i) = i$ gives the spanning tree ${(0,1), (0,2), (0,3)} subset.eq E(K_4)$. + + #figure({ + let blue = graph-colors.at(0) + let gray = luma(200) + canvas(length: 1cm, { + import draw: * + // G = K4 on the left + let gv = ((0, 0), (1.5, 0), (1.5, 1.5), (0, 1.5)) + let ge = ((0,1),(0,2),(0,3),(1,2),(1,3),(2,3)) + let tree-edges = ((0,1),(0,2),(0,3)) + for (u, v) in ge { + let is-tree = tree-edges.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)) + g-edge(gv.at(u), gv.at(v), stroke: if is-tree { 2pt + blue } else { 1pt + gray }) + } + for (k, pos) in gv.enumerate() { + let is-center = k == 0 + g-node(pos, name: "g" + str(k), + fill: if is-center { blue } else { white }, + label: if is-center { text(fill: white)[$v_#k$] } else { [$v_#k$] }) + } + // Arrow + content((2.5, 0.75), text(10pt)[$arrow.l.double$]) + // T = star S3 on the right + let tv = ((3.5, 0.75), (5.0, 0), (5.0, 0.75), (5.0, 1.5)) + let te = ((0,1),(0,2),(0,3)) + for (u, v) in te { + g-edge(tv.at(u), tv.at(v), stroke: 2pt + blue) + } + for (k, pos) in tv.enumerate() { + let is-center = k == 0 + g-node(pos, name: "t" + str(k), + fill: if is-center { blue } else { white }, + label: if is-center { text(fill: white)[$u_#k$] } else { [$u_#k$] }) + } + }) + }, + caption: [Isomorphic Spanning Tree: the graph $G = K_4$ (left) contains a spanning tree isomorphic to the star $S_3$ (right, blue edges). The identity mapping $pi(u_i) = v_i$ embeds all three star edges into $G$. Center vertex $v_0$ shown in blue.], + ) +] #problem-def("KColoring")[ Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$. ][ @@ -1038,6 +1090,66 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa *Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$. ] +#problem-def("ShortestCommonSupersequence")[ + Given a finite alphabet $Sigma$, a set $R = {r_1, dots, r_m}$ of strings over $Sigma^*$, and a positive integer $K$, determine whether there exists a string $w in Sigma^*$ with $|w| lt.eq K$ such that every string $r_i in R$ is a _subsequence_ of $w$: there exist indices $1 lt.eq j_1 < j_2 < dots < j_(|r_i|) lt.eq |w|$ with $w[j_k] = r_i [k]$ for all $k$. +][ + A classic NP-complete string problem, listed as problem SR8 in Garey and Johnson @garey1979. #cite(, form: "prose") proved NP-completeness; #cite(, form: "prose") showed the problem remains NP-complete even over a binary alphabet ($|Sigma| = 2$). Note that _subsequence_ (characters may be non-contiguous) differs from _substring_ (contiguous block): the Shortest Common Supersequence asks that each input string can be embedded into $w$ by selecting characters in order but not necessarily adjacently. + + For $|R| = 2$ strings, the problem is solvable in polynomial time via the duality with the Longest Common Subsequence (LCS): if $"LCS"(r_1, r_2)$ has length $ell$, then the shortest common supersequence has length $|r_1| + |r_2| - ell$, computable in $O(|r_1| dot |r_2|)$ time by dynamic programming. For general $|R| = m$, the brute-force search over all strings of length at most $K$ takes $O(|Sigma|^K)$ time. Applications include bioinformatics (reconstructing ancestral sequences from fragments), data compression (representing multiple strings compactly), and scheduling (merging instruction sequences). + + *Example.* Let $Sigma = {a, b, c}$ and $R = {"abc", "bac"}$. We seek the shortest string $w$ containing both $"abc"$ and $"bac"$ as subsequences. + + #figure({ + let w = ("b", "a", "b", "c") + let r1 = ("a", "b", "c") // "abc" + let r2 = ("b", "a", "c") // "bac" + let embed1 = (1, 2, 3) // positions of a, b, c in w (0-indexed) + let embed2 = (0, 1, 3) // positions of b, a, c in w (0-indexed) + let blue = graph-colors.at(0) + let teal = rgb("#76b7b2") + let red = graph-colors.at(1) + align(center, stack(dir: ttb, spacing: 0.6cm, + // Row 1: the supersequence w + stack(dir: ltr, spacing: 0pt, + box(width: 1.2cm, height: 0.5cm, align(center + horizon, text(8pt)[$w =$])), + ..w.enumerate().map(((i, ch)) => { + let is1 = embed1.contains(i) + let is2 = embed2.contains(i) + let fill = if is1 and is2 { blue.transparentize(60%) } else if is1 { blue.transparentize(80%) } else if is2 { teal.transparentize(80%) } else { white } + box(width: 0.55cm, height: 0.55cm, fill: fill, stroke: 0.5pt + luma(120), + align(center + horizon, text(9pt, weight: "bold", ch))) + }), + ), + // Row 2: embedding of r1 + stack(dir: ltr, spacing: 0pt, + box(width: 1.2cm, height: 0.5cm, align(center + horizon, text(8pt, fill: blue)[$r_1 =$])), + ..range(w.len()).map(i => { + let idx = embed1.position(j => j == i) + let ch = if idx != none { r1.at(idx) } else { sym.dot.c } + let col = if idx != none { blue } else { luma(200) } + box(width: 0.55cm, height: 0.55cm, + align(center + horizon, text(9pt, fill: col, weight: if idx != none { "bold" } else { "regular" }, ch))) + }), + ), + // Row 3: embedding of r2 + stack(dir: ltr, spacing: 0pt, + box(width: 1.2cm, height: 0.5cm, align(center + horizon, text(8pt, fill: teal)[$r_2 =$])), + ..range(w.len()).map(i => { + let idx = embed2.position(j => j == i) + let ch = if idx != none { r2.at(idx) } else { sym.dot.c } + let col = if idx != none { teal } else { luma(200) } + box(width: 0.55cm, height: 0.55cm, + align(center + horizon, text(9pt, fill: col, weight: if idx != none { "bold" } else { "regular" }, ch))) + }), + ), + )) + }, + caption: [Shortest Common Supersequence: $w = "babc"$ (length 4) contains $r_1 = "abc"$ (blue, positions 1,2,3) and $r_2 = "bac"$ (teal, positions 0,1,3) as subsequences. Dots mark unused positions in each embedding.], + ) + + The supersequence $w = "babc"$ has length 4 and contains both input strings as subsequences. This is optimal because $"LCS"("abc", "bac") = "ac"$ (length 2), so the shortest common supersequence has length $3 + 3 - 2 = 4$. +] + #problem-def("MinimumFeedbackArcSet")[ Given a directed graph $G = (V, A)$, find a minimum-size subset $A' subset.eq A$ such that $G - A'$ is a directed acyclic graph (DAG). Equivalently, $A'$ must contain at least one arc from every directed cycle in $G$. ][ @@ -1046,6 +1158,77 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa *Example.* Consider $G$ with $V = {0, 1, 2, 3, 4, 5}$ and arcs $(0 arrow 1), (1 arrow 2), (2 arrow 0), (1 arrow 3), (3 arrow 4), (4 arrow 1), (2 arrow 5), (5 arrow 3), (3 arrow 0)$. This graph contains four directed cycles: $0 arrow 1 arrow 2 arrow 0$, $1 arrow 3 arrow 4 arrow 1$, $0 arrow 1 arrow 3 arrow 0$, and $2 arrow 5 arrow 3 arrow 0 arrow 1 arrow 2$. Removing $A' = {(0 arrow 1), (3 arrow 4)}$ breaks all four cycles (vertex 0 becomes a sink in the residual graph), giving a minimum FAS of size 2. ] +#problem-def("FlowShopScheduling")[ + Given $m$ processors and a set $J$ of $n$ jobs, where each job $j in J$ consists of $m$ tasks $t_1 [j], t_2 [j], dots, t_m [j]$ with lengths $ell(t_i [j]) in ZZ^+_0$, and a deadline $D in ZZ^+$, determine whether there exists a permutation schedule $pi$ of the jobs such that all jobs complete by time $D$. Each job must be processed on machines $1, 2, dots, m$ in order, and job $j$ cannot start on machine $i+1$ until its task on machine $i$ is completed. +][ + Flow Shop Scheduling is a classical NP-complete problem from Garey & Johnson (A5 SS15), strongly NP-hard for $m >= 3$ @garey1976. For $m = 2$, it is solvable in $O(n log n)$ by Johnson's rule @johnson1954. The problem is fundamental in operations research, manufacturing planning, and VLSI design. When restricted to permutation schedules (same job order on all machines), the search space is $n!$ orderings. The best known exact algorithm for $m = 3$ runs in $O^*(3^n)$ time @shang2018; for general $m$, brute-force over $n!$ permutations gives $O(n! dot m n)$. + + *Example.* Let $m = 3$ machines, $n = 5$ jobs with task lengths: + $ ell = mat( + 3, 4, 2; + 2, 3, 5; + 4, 1, 3; + 1, 5, 4; + 3, 2, 3; + ) $ + and deadline $D = 25$. The job order $pi = (j_4, j_1, j_5, j_3, j_2)$ (0-indexed: $3, 0, 4, 2, 1$) yields makespan $23 <= 25$, so a feasible schedule exists. + + #figure( + canvas(length: 1cm, { + import draw: * + // Gantt chart for job order [3, 0, 4, 2, 1] on 3 machines + // Schedule computed greedily: + // M1: j3[0,1], j0[1,4], j4[4,7], j2[7,11], j1[11,13] + // M2: j3[1,6], j0[6,10], j4[10,12], j2[12,13], j1[13,16] + // M3: j3[6,10], j0[10,12], j4[12,15], j2[15,18], j1[18,23] + let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f")) + let job-names = ("$j_1$", "$j_2$", "$j_3$", "$j_4$", "$j_5$") + let scale = 0.38 + let row-h = 0.6 + let gap = 0.15 + + // Machine labels + for (mi, label) in ("M1", "M2", "M3").enumerate() { + let y = -mi * (row-h + gap) + content((-0.8, y), text(8pt, label)) + } + + // Draw schedule blocks: (machine, job-index, start, end) + let blocks = ( + (0, 3, 0, 1), (0, 0, 1, 4), (0, 4, 4, 7), (0, 2, 7, 11), (0, 1, 11, 13), + (1, 3, 1, 6), (1, 0, 6, 10), (1, 4, 10, 12), (1, 2, 12, 13), (1, 1, 13, 16), + (2, 3, 6, 10), (2, 0, 10, 12), (2, 4, 12, 15), (2, 2, 15, 18), (2, 1, 18, 23), + ) + + for (mi, ji, s, e) in blocks { + let x0 = s * scale + let x1 = e * scale + let y = -mi * (row-h + gap) + rect((x0, y - row-h / 2), (x1, y + row-h / 2), + fill: colors.at(ji).transparentize(30%), stroke: 0.4pt + colors.at(ji)) + content(((x0 + x1) / 2, y), text(6pt, job-names.at(ji))) + } + + // Time axis + let max-t = 23 + let y-axis = -2 * (row-h + gap) - row-h / 2 - 0.2 + line((0, y-axis), (max-t * scale, y-axis), stroke: 0.4pt) + for t in (0, 5, 10, 15, 20, 23) { + let x = t * scale + line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt) + content((x, y-axis - 0.25), text(6pt, str(t))) + } + content((max-t * scale / 2, y-axis - 0.5), text(7pt)[$t$]) + + // Deadline marker + let dl-x = 25 * scale + line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed")) + content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = 25$]) + }), + caption: [Flow shop schedule for 5 jobs on 3 machines. Job order $(j_4, j_1, j_5, j_3, j_2)$ achieves makespan 23, within deadline $D = 25$ (dashed red line).], + ) +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index d73afb4e3..88bbc3723 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -489,6 +489,17 @@ @article{cygan2014 doi = {10.1137/140990255} } +@article{raiha1981, + author = {Kari-Jouko R{\"a}ih{\"a} and Esko Ukkonen}, + title = {The Shortest Common Supersequence Problem over Binary Alphabet is {NP}-Complete}, + journal = {Theoretical Computer Science}, + volume = {16}, + number = {2}, + pages = {187--198}, + year = {1981}, + doi = {10.1016/0304-3975(81)90075-X} +} + @article{bodlaender2012, author = {Hans L. Bodlaender and Fedor V. Fomin and Arie M. C. A. Koster and Dieter Kratsch and Dimitrios M. Thilikos}, title = {A Note on Exact Algorithms for Vertex Ordering Problems on Graphs}, @@ -521,3 +532,14 @@ @article{lucchesi1978 year = {1978}, doi = {10.1112/jlms/s2-17.3.369} } + +@article{papadimitriou1982, + author = {Christos H. Papadimitriou and Mihalis Yannakakis}, + title = {The Complexity of Restricted Spanning Tree Problems}, + journal = {Journal of the ACM}, + volume = {29}, + number = {2}, + pages = {285--309}, + year = {1982}, + doi = {10.1145/322307.322309} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 03bee10c8..e8f6f968a 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -158,6 +158,22 @@ } ] }, + { + "name": "IsomorphicSpanningTree", + "description": "Does graph G contain a spanning tree isomorphic to tree T?", + "fields": [ + { + "name": "graph", + "type_name": "SimpleGraph", + "description": "The host graph G" + }, + { + "name": "tree", + "type_name": "SimpleGraph", + "description": "The target tree T (must be a tree with |V(T)| = |V(G)|)" + } + ] + }, { "name": "KColoring", "description": "Find valid k-coloring of a graph", @@ -488,6 +504,27 @@ } ] }, + { + "name": "ShortestCommonSupersequence", + "description": "Find a common supersequence of bounded length for a set of strings", + "fields": [ + { + "name": "alphabet_size", + "type_name": "usize", + "description": "Size of the alphabet" + }, + { + "name": "strings", + "type_name": "Vec>", + "description": "Input strings over the alphabet {0, ..., alphabet_size-1}" + }, + { + "name": "bound", + "type_name": "usize", + "description": "Bound on supersequence length (configuration has exactly this many symbols)" + } + ] + }, { "name": "SpinGlass", "description": "Minimize Ising Hamiltonian on a graph", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index ae65be85e..64d98d723 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -210,6 +210,7 @@ Flags by problem type: KColoring --graph, --k PartitionIntoTriangles --graph GraphPartitioning --graph + IsomorphicSpanningTree --graph, --tree Factoring --target, --m, --n BinPacking --sizes, --capacity SubsetSum --sizes, --target @@ -224,6 +225,8 @@ Flags by problem type: LCS --strings FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] + FlowShopScheduling --task-lengths, --deadline [--num-processors] + SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -335,21 +338,36 @@ pub struct CreateArgs { /// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10] #[arg(long, allow_hyphen_values = true)] pub bounds: Option, + /// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3) + #[arg(long)] + pub tree: Option, /// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4") #[arg(long)] pub required_edges: Option, - /// Upper bound B for RuralPostman + /// Upper bound (for RuralPostman or SCS) #[arg(long)] - pub bound: Option, + pub bound: Option, /// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0) #[arg(long)] pub pattern: Option, - /// Input strings for LCS (semicolon-separated, e.g., "ABAC;BACA") + /// Input strings for LCS (e.g., "ABAC;BACA") or SCS (e.g., "0,1,2;1,2,0") #[arg(long)] pub strings: Option, /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") + #[arg(long)] + pub task_lengths: Option, + /// Deadline for FlowShopScheduling + #[arg(long)] + pub deadline: Option, + /// Number of processors/machines for FlowShopScheduling + #[arg(long)] + pub num_processors: Option, + /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) + #[arg(long)] + pub alphabet_size: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index beb1113d0..c7a809919 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,10 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; -use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum}; +use problemreductions::models::misc::{ + BinPacking, FlowShopScheduling, LongestCommonSubsequence, PaintShop, + ShortestCommonSupersequence, SubsetSum, +}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -47,11 +50,16 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.basis.is_none() && args.target_vec.is_none() && args.bounds.is_none() + && args.tree.is_none() && args.required_edges.is_none() && args.bound.is_none() && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.task_lengths.is_none() + && args.deadline.is_none() + && args.num_processors.is_none() + && args.alphabet_size.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -86,6 +94,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { }, "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "HamiltonianPath" => "--graph 0-1,1-2,2-3", + "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" } @@ -94,7 +103,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "QUBO" => "--matrix \"1,0.5;0.5,2\"", "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", - "MinimumSumMulticenter" => "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2", + "MinimumSumMulticenter" => { + "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" + } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", @@ -103,6 +114,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", + "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", } } @@ -231,13 +243,47 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // Hamiltonian path (graph only, no weights) "HamiltonianPath" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!("{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3") + })?; + (ser(HamiltonianPath::new(graph))?, resolved_variant.clone()) + } + + // IsomorphicSpanningTree (graph + tree) + "IsomorphicSpanningTree" => { let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( - "{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3" + "{e}\n\nUsage: pred create IsomorphicSpanningTree --graph 0-1,1-2,0-2 --tree 0-1,1-2" ) })?; + let tree_str = args.tree.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "IsomorphicSpanningTree requires --tree\n\n\ + Usage: pred create IsomorphicSpanningTree --graph 0-1,1-2,0-2 --tree 0-1,1-2" + ) + })?; + let tree_edges: Vec<(usize, usize)> = tree_str + .split(',') + .map(|pair| { + let parts: Vec<&str> = pair.trim().split('-').collect(); + if parts.len() != 2 { + bail!("Invalid tree edge '{}': expected format u-v", pair.trim()); + } + let u: usize = parts[0].parse()?; + let v: usize = parts[1].parse()?; + Ok((u, v)) + }) + .collect::>>()?; + let tree_num_vertices = tree_edges + .iter() + .flat_map(|(u, v)| [*u, *v]) + .max() + .map(|m| m + 1) + .unwrap_or(0) + .max(graph.num_vertices()); + let tree = SimpleGraph::new(tree_num_vertices, tree_edges); ( - ser(HamiltonianPath::new(graph))?, + ser(problemreductions::models::graph::IsomorphicSpanningTree::new(graph, tree))?, resolved_variant.clone(), ) } @@ -280,7 +326,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "RuralPostman requires --bound\n\n\ Usage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6" ) - })?; + })? as i32; ( ser(RuralPostman::new( graph, @@ -563,6 +609,53 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // FlowShopScheduling + "FlowShopScheduling" => { + let task_str = args.task_lengths.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "FlowShopScheduling requires --task-lengths and --deadline\n\n\ + Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3" + ) + })?; + let deadline = args.deadline.ok_or_else(|| { + anyhow::anyhow!( + "FlowShopScheduling requires --deadline\n\n\ + Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3" + ) + })?; + let task_lengths: Vec> = task_str + .split(';') + .map(|row| util::parse_comma_list(row.trim())) + .collect::>>()?; + let num_processors = if let Some(np) = args.num_processors { + np + } else if let Some(m) = args.m { + m + } else if let Some(first) = task_lengths.first() { + first.len() + } else { + bail!("Cannot infer num_processors from empty task list; use --num-processors"); + }; + for (j, row) in task_lengths.iter().enumerate() { + if row.len() != num_processors { + bail!( + "task_lengths row {} has {} entries, expected {} (num_processors)", + j, + row.len(), + num_processors + ); + } + } + ( + ser(FlowShopScheduling::new( + num_processors, + task_lengths, + deadline, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackArcSet "MinimumFeedbackArcSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { @@ -667,6 +760,57 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // ShortestCommonSupersequence + "ShortestCommonSupersequence" => { + let usage = "Usage: pred create SCS --strings \"0,1,2;1,2,0\" --bound 4"; + let strings_str = args.strings.as_deref().ok_or_else(|| { + anyhow::anyhow!("ShortestCommonSupersequence requires --strings\n\n{usage}") + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!("ShortestCommonSupersequence requires --bound\n\n{usage}") + })? as usize; + let strings: Vec> = strings_str + .split(';') + .map(|s| { + let trimmed = s.trim(); + if trimmed.is_empty() { + return Ok(Vec::new()); + } + trimmed + .split(',') + .map(|v| { + v.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid alphabet index: {}", e)) + }) + .collect::>>() + }) + .collect::>>()?; + let inferred = strings + .iter() + .flat_map(|s| s.iter()) + .copied() + .max() + .map(|m| m + 1) + .unwrap_or(0); + let alphabet_size = args.alphabet_size.unwrap_or(inferred); + if alphabet_size < inferred { + anyhow::bail!( + "--alphabet-size {} is smaller than the largest symbol + 1 ({}) in the strings", + alphabet_size, + inferred + ); + } + ( + ser(ShortestCommonSupersequence::new( + alphabet_size, + strings, + bound, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackVertexSet "MinimumFeedbackVertexSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 480f05fc7..2d95f3bb6 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,9 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, LongestCommonSubsequence, SubsetSum}; +use problemreductions::models::misc::{ + BinPacking, FlowShopScheduling, Knapsack, LongestCommonSubsequence, + ShortestCommonSupersequence, SubsetSum, +}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -213,6 +216,9 @@ pub fn load_problem( "MinimumSumMulticenter" => deser_opt::>(data), "GraphPartitioning" => deser_opt::>(data), "HamiltonianPath" => deser_sat::>(data), + "IsomorphicSpanningTree" => { + deser_sat::(data) + } "MaxCut" => deser_opt::>(data), "MaximalIS" => deser_opt::>(data), "TravelingSalesman" => deser_opt::>(data), @@ -253,7 +259,9 @@ pub fn load_problem( "PartitionIntoTriangles" => deser_sat::>(data), "LongestCommonSubsequence" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), + "FlowShopScheduling" => deser_sat::(data), "SubsetSum" => deser_sat::(data), + "ShortestCommonSupersequence" => deser_sat::(data), "MinimumFeedbackArcSet" => deser_opt::>(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } @@ -280,6 +288,9 @@ pub fn serialize_any_problem( "MinimumSumMulticenter" => try_ser::>(any), "GraphPartitioning" => try_ser::>(any), "HamiltonianPath" => try_ser::>(any), + "IsomorphicSpanningTree" => { + try_ser::(any) + } "MaxCut" => try_ser::>(any), "MaximalIS" => try_ser::>(any), "TravelingSalesman" => try_ser::>(any), @@ -323,7 +334,9 @@ pub fn serialize_any_problem( "PartitionIntoTriangles" => try_ser::>(any), "LongestCommonSubsequence" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), + "FlowShopScheduling" => try_ser::(any), "SubsetSum" => try_ser::(any), + "ShortestCommonSupersequence" => try_ser::(any), "MinimumFeedbackArcSet" => try_ser::>(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 2933149fb..2485467c2 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -2,8 +2,8 @@ use crate::util; use problemreductions::models::algebraic::QUBO; use problemreductions::models::formula::{CNFClause, Satisfiability}; use problemreductions::models::graph::{ - MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumSumMulticenter, - MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, + MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, + MinimumSumMulticenter, MinimumVertexCover, SpinGlass, TravelingSalesman, }; use problemreductions::models::misc::Factoring; use problemreductions::registry::collect_schemas; @@ -528,7 +528,12 @@ impl McpServer { })?; let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); ( - ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, + ser(MinimumSumMulticenter::new( + graph, + vertex_weights, + edge_lengths, + k, + ))?, variant, ) } @@ -672,7 +677,12 @@ impl McpServer { .unwrap_or(1.max(num_vertices / 3)); let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); ( - ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, + ser(MinimumSumMulticenter::new( + graph, + vertex_weights, + edge_lengths, + k, + ))?, variant, ) } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index b01fe0060..55d1cf900 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -24,6 +24,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("LCS", "LongestCommonSubsequence"), ("MaxMatching", "MaximumMatching"), ("FVS", "MinimumFeedbackVertexSet"), + ("SCS", "ShortestCommonSupersequence"), ("FAS", "MinimumFeedbackArcSet"), ("pmedian", "MinimumSumMulticenter"), ]; @@ -38,6 +39,7 @@ pub fn resolve_alias(input: &str) -> String { "ksat" | "ksatisfiability" => "KSatisfiability".to_string(), "qubo" => "QUBO".to_string(), "graphpartitioning" => "GraphPartitioning".to_string(), + "isomorphicspanningtree" => "IsomorphicSpanningTree".to_string(), "maxcut" => "MaxCut".to_string(), "spinglass" => "SpinGlass".to_string(), "ilp" => "ILP".to_string(), @@ -63,9 +65,11 @@ pub fn resolve_alias(input: &str) -> String { "partitionintotriangles" => "PartitionIntoTriangles".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "flowshopscheduling" => "FlowShopScheduling".to_string(), "fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(), "minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(), "subsetsum" => "SubsetSum".to_string(), + "scs" | "shortestcommonsupersequence" => "ShortestCommonSupersequence".to_string(), "hamiltonianpath" => "HamiltonianPath".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/lib.rs b/src/lib.rs index 6d9cd9a1d..4e0596ee2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,16 +41,18 @@ pub mod prelude { pub use crate::models::algebraic::{BMF, QUBO}; pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use crate::models::graph::{ - BicliqueCover, GraphPartitioning, HamiltonianPath, SpinGlass, SubgraphIsomorphism, + BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass, + SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, - MinimumSumMulticenter, MinimumVertexCover, - PartitionIntoTriangles, RuralPostman, TravelingSalesman, + MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles, RuralPostman, + TravelingSalesman, }; pub use crate::models::misc::{ - BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum, + BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, + ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs new file mode 100644 index 000000000..85a4d7936 --- /dev/null +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -0,0 +1,171 @@ +//! Isomorphic Spanning Tree problem implementation. +//! +//! Given a graph G and a tree T with |V(G)| = |V(T)|, determine whether G +//! contains a spanning tree isomorphic to T. This is a classical NP-complete +//! problem (Garey & Johnson, ND8) that generalizes Hamiltonian Path. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "IsomorphicSpanningTree", + module_path: module_path!(), + description: "Does graph G contain a spanning tree isomorphic to tree T?", + fields: &[ + FieldInfo { name: "graph", type_name: "SimpleGraph", description: "The host graph G" }, + FieldInfo { name: "tree", type_name: "SimpleGraph", description: "The target tree T (must be a tree with |V(T)| = |V(G)|)" }, + ], + } +} + +/// Isomorphic Spanning Tree problem. +/// +/// Given an undirected graph G = (V, E) and a tree T = (V_T, E_T) with +/// |V| = |V_T|, determine if there exists a bijection π: V_T → V such that +/// for every edge {u, v} in E_T, {π(u), π(v)} is an edge in E. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::graph::IsomorphicSpanningTree; +/// use problemreductions::topology::SimpleGraph; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Host graph: triangle 0-1-2-0 +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); +/// // Tree: path 0-1-2 +/// let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); +/// let problem = IsomorphicSpanningTree::new(graph, tree); +/// +/// let solver = BruteForce::new(); +/// let sol = solver.find_satisfying(&problem); +/// assert!(sol.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IsomorphicSpanningTree { + graph: SimpleGraph, + tree: SimpleGraph, +} + +impl IsomorphicSpanningTree { + /// Create a new IsomorphicSpanningTree problem. + /// + /// # Panics + /// + /// Panics if |V(G)| != |V(T)| or if T is not a tree (not connected or + /// wrong number of edges). + pub fn new(graph: SimpleGraph, tree: SimpleGraph) -> Self { + let n = graph.num_vertices(); + assert_eq!( + n, + tree.num_vertices(), + "graph and tree must have the same number of vertices" + ); + if n > 0 { + assert_eq!(tree.num_edges(), n - 1, "tree must have exactly n-1 edges"); + assert!(Self::is_connected(&tree), "tree must be connected"); + } + Self { graph, tree } + } + + /// Get a reference to the host graph. + pub fn graph(&self) -> &SimpleGraph { + &self.graph + } + + /// Get a reference to the target tree. + pub fn tree(&self) -> &SimpleGraph { + &self.tree + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the host graph. + pub fn num_graph_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Get the number of edges in the target tree. + pub fn num_tree_edges(&self) -> usize { + self.tree.num_edges() + } + + /// Check if a graph is connected using BFS. + fn is_connected(graph: &SimpleGraph) -> bool { + let n = graph.num_vertices(); + if n == 0 { + return true; + } + let mut visited = vec![false; n]; + let mut queue = std::collections::VecDeque::new(); + visited[0] = true; + queue.push_back(0); + let mut count = 1; + while let Some(v) = queue.pop_front() { + for u in graph.neighbors(v) { + if !visited[u] { + visited[u] = true; + count += 1; + queue.push_back(u); + } + } + } + count == n + } +} + +impl Problem for IsomorphicSpanningTree { + const NAME: &'static str = "IsomorphicSpanningTree"; + type Metric = bool; + + fn dims(&self) -> Vec { + let n = self.graph.num_vertices(); + vec![n; n] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.graph.num_vertices(); + if config.len() != n { + return false; + } + + // Check that config is a valid permutation: all values in 0..n, all distinct + let mut seen = vec![false; n]; + for &v in config { + if v >= n || seen[v] { + return false; + } + seen[v] = true; + } + + // Check that every tree edge maps to a graph edge under the permutation + // config[i] = π(i): tree vertex i maps to graph vertex config[i] + for (u, v) in self.tree.edges() { + if !self.graph.has_edge(config[u], config[v]) { + return false; + } + } + + true + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for IsomorphicSpanningTree {} + +crate::declare_variants! { + IsomorphicSpanningTree => "num_vertices^num_vertices", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/isomorphic_spanning_tree.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index b78522992..e49e4ca5d 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -9,6 +9,7 @@ //! - [`MaximumClique`]: Maximum weight clique //! - [`MaxCut`]: Maximum cut on weighted graphs //! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) +//! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) //! - [`KColoring`]: K-vertex coloring //! - [`PartitionIntoTriangles`]: Partition vertices into triangles //! - [`MaximumMatching`]: Maximum weight matching @@ -24,16 +25,17 @@ pub(crate) mod biclique_cover; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_path; +pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kcoloring; pub(crate) mod max_cut; pub(crate) mod maximal_is; pub(crate) mod maximum_clique; pub(crate) mod maximum_independent_set; pub(crate) mod maximum_matching; -pub(crate) mod minimum_sum_multicenter; pub(crate) mod minimum_dominating_set; pub(crate) mod minimum_feedback_arc_set; pub(crate) mod minimum_feedback_vertex_set; +pub(crate) mod minimum_sum_multicenter; pub(crate) mod minimum_vertex_cover; pub(crate) mod partition_into_triangles; pub(crate) mod rural_postman; @@ -44,16 +46,17 @@ pub(crate) mod traveling_salesman; pub use biclique_cover::BicliqueCover; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_path::HamiltonianPath; +pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kcoloring::KColoring; pub use max_cut::MaxCut; pub use maximal_is::MaximalIS; pub use maximum_clique::MaximumClique; pub use maximum_independent_set::MaximumIndependentSet; pub use maximum_matching::MaximumMatching; -pub use minimum_sum_multicenter::MinimumSumMulticenter; pub use minimum_dominating_set::MinimumDominatingSet; pub use minimum_feedback_arc_set::MinimumFeedbackArcSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; +pub use minimum_sum_multicenter::MinimumSumMulticenter; pub use minimum_vertex_cover::MinimumVertexCover; pub use partition_into_triangles::PartitionIntoTriangles; pub use rural_postman::RuralPostman; diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs new file mode 100644 index 000000000..6568cced8 --- /dev/null +++ b/src/models/misc/flow_shop_scheduling.rs @@ -0,0 +1,201 @@ +//! Flow Shop Scheduling problem implementation. +//! +//! Given m processors and a set of jobs, each consisting of m tasks (one per processor) +//! that must be processed in processor order 1, 2, ..., m, determine if all jobs can +//! be completed by a global deadline D. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "FlowShopScheduling", + module_path: module_path!(), + description: "Determine if a flow-shop schedule for jobs on m processors meets a deadline", + fields: &[ + FieldInfo { name: "num_processors", type_name: "usize", description: "Number of machines m" }, + FieldInfo { name: "task_lengths", type_name: "Vec>", description: "task_lengths[j][i] = length of job j's task on machine i" }, + FieldInfo { name: "deadline", type_name: "u64", description: "Global deadline D" }, + ], + } +} + +/// The Flow Shop Scheduling problem. +/// +/// Given `m` processors and a set of `n` jobs, each job `j` consists of `m` tasks +/// `t_1[j], t_2[j], ..., t_m[j]` with specified lengths. Tasks must be processed +/// in processor order: job `j` cannot start on machine `i+1` until its task on +/// machine `i` is completed. The question is whether there exists a schedule such +/// that all jobs complete by deadline `D`. +/// +/// # Representation +/// +/// Configurations use Lehmer code encoding with `dims() = [n, n-1, ..., 1]`. +/// A config `[c_0, c_1, ..., c_{n-1}]` where `c_i < n - i` is decoded by +/// maintaining a list of available jobs and picking the `c_i`-th element: +/// +/// For 3 jobs, config `[2, 0, 0]`: available=`[0,1,2]`, pick index 2 → job 2; +/// available=`[0,1]`, pick index 0 → job 0; available=`[1]`, pick index 0 → job 1. +/// Result: job order `[2, 0, 1]`. +/// +/// Given a job order, start times are determined greedily (as early as possible). +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::FlowShopScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 2 machines, 3 jobs, deadline 10 +/// let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FlowShopScheduling { + /// Number of processors (machines). + num_processors: usize, + /// Task lengths: `task_lengths[j][i]` is the processing time of job `j` on machine `i`. + task_lengths: Vec>, + /// Global deadline. + deadline: u64, +} + +impl FlowShopScheduling { + /// Create a new Flow Shop Scheduling instance. + /// + /// # Arguments + /// * `num_processors` - Number of machines m + /// * `task_lengths` - task_lengths[j][i] = processing time of job j on machine i. + /// Each inner Vec must have length `num_processors`. + /// * `deadline` - Global deadline D + /// + /// # Panics + /// Panics if any job does not have exactly `num_processors` tasks. + pub fn new(num_processors: usize, task_lengths: Vec>, deadline: u64) -> Self { + for (j, tasks) in task_lengths.iter().enumerate() { + assert_eq!( + tasks.len(), + num_processors, + "Job {} has {} tasks, expected {}", + j, + tasks.len(), + num_processors + ); + } + Self { + num_processors, + task_lengths, + deadline, + } + } + + /// Get the number of processors. + pub fn num_processors(&self) -> usize { + self.num_processors + } + + /// Get the task lengths matrix. + pub fn task_lengths(&self) -> &[Vec] { + &self.task_lengths + } + + /// Get the deadline. + pub fn deadline(&self) -> u64 { + self.deadline + } + + /// Get the number of jobs. + pub fn num_jobs(&self) -> usize { + self.task_lengths.len() + } + + /// Compute the makespan for a given job ordering. + /// + /// The job_order slice must be a permutation of `0..num_jobs`. + /// Returns the completion time of the last job on the last machine. + pub fn compute_makespan(&self, job_order: &[usize]) -> u64 { + let n = job_order.len(); + let m = self.num_processors; + assert_eq!( + n, + self.task_lengths.len(), + "job_order length ({}) does not match num_jobs ({})", + n, + self.task_lengths.len() + ); + for (k, &job) in job_order.iter().enumerate() { + assert!( + job < self.task_lengths.len(), + "job_order[{}] = {} is out of range (num_jobs = {})", + k, + job, + self.task_lengths.len() + ); + } + if n == 0 || m == 0 { + return 0; + } + + // completion[k][i] = completion time of the k-th job in sequence on machine i + let mut completion = vec![vec![0u64; m]; n]; + + for (k, &job) in job_order.iter().enumerate() { + for i in 0..m { + let prev_machine = if i == 0 { 0 } else { completion[k][i - 1] }; + let prev_job = if k == 0 { 0 } else { completion[k - 1][i] }; + let start = prev_machine.max(prev_job); + completion[k][i] = start + self.task_lengths[job][i]; + } + } + + completion[n - 1][m - 1] + } +} + +impl Problem for FlowShopScheduling { + const NAME: &'static str = "FlowShopScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + let n = self.num_jobs(); + (0..n).rev().map(|i| i + 1).collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.num_jobs(); + if config.len() != n { + return false; + } + + // Decode Lehmer code into a permutation. + // config[i] must be < n - i (the domain size for position i). + let mut available: Vec = (0..n).collect(); + let mut job_order = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return false; + } + job_order.push(available.remove(c)); + } + + let makespan = self.compute_makespan(&job_order); + makespan <= self.deadline + } +} + +impl SatisfactionProblem for FlowShopScheduling {} + +crate::declare_variants! { + FlowShopScheduling => "factorial(num_jobs)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/flow_shop_scheduling.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 943b758a2..5ce862ad4 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -3,21 +3,27 @@ //! Problems with unique input structures that don't fit other categories: //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization +//! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; pub(crate) mod factoring; +mod flow_shop_scheduling; mod knapsack; mod longest_common_subsequence; pub(crate) mod paintshop; +pub(crate) mod shortest_common_supersequence; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; +pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use paintshop::PaintShop; +pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs new file mode 100644 index 000000000..a6da920f0 --- /dev/null +++ b/src/models/misc/shortest_common_supersequence.rs @@ -0,0 +1,154 @@ +//! Shortest Common Supersequence problem implementation. +//! +//! Given a set of strings over an alphabet and a bound `B`, the problem asks +//! whether there exists a common supersequence of length at most `B`. A string +//! `w` is a supersequence of `s` if `s` is a subsequence of `w` (i.e., `s` can +//! be obtained by deleting zero or more characters from `w`). +//! +//! The configuration uses a fixed-length representation of exactly `B` symbols. +//! Since any supersequence shorter than `B` can be padded with an arbitrary +//! symbol to reach length `B` (when `alphabet_size > 0`), this is equivalent +//! to the standard `|w| ≤ B` formulation. This problem is NP-hard (Maier, 1978). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "ShortestCommonSupersequence", + module_path: module_path!(), + description: "Find a common supersequence of bounded length for a set of strings", + fields: &[ + FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the alphabet" }, + FieldInfo { name: "strings", type_name: "Vec>", description: "Input strings over the alphabet {0, ..., alphabet_size-1}" }, + FieldInfo { name: "bound", type_name: "usize", description: "Bound on supersequence length (configuration has exactly this many symbols)" }, + ], + } +} + +/// The Shortest Common Supersequence problem. +/// +/// Given an alphabet of size `k`, a set of strings over `{0, ..., k-1}`, and a +/// bound `B`, determine whether there exists a string `w` of length at most `B` +/// such that every input string is a subsequence of `w`. The configuration uses +/// exactly `B` symbols (equivalent via padding when `alphabet_size > 0`). +/// +/// # Representation +/// +/// The configuration is a vector of length `bound`, where each entry is a symbol +/// in `{0, ..., alphabet_size-1}`. The problem is satisfiable iff every input +/// string is a subsequence of the configuration. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::ShortestCommonSupersequence; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Alphabet {0, 1}, strings [0,1] and [1,0], bound 3 +/// let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortestCommonSupersequence { + alphabet_size: usize, + strings: Vec>, + bound: usize, +} + +impl ShortestCommonSupersequence { + /// Create a new ShortestCommonSupersequence instance. + /// + /// # Panics + /// + /// Panics if `alphabet_size` is 0 and any input string is non-empty, or if + /// `bound > 0` and `alphabet_size == 0`. + pub fn new(alphabet_size: usize, strings: Vec>, bound: usize) -> Self { + assert!( + alphabet_size > 0 || (bound == 0 && strings.iter().all(|s| s.is_empty())), + "alphabet_size must be > 0 when bound > 0 or any input string is non-empty" + ); + Self { + alphabet_size, + strings, + bound, + } + } + + /// Returns the alphabet size. + pub fn alphabet_size(&self) -> usize { + self.alphabet_size + } + + /// Returns the input strings. + pub fn strings(&self) -> &[Vec] { + &self.strings + } + + /// Returns the bound on supersequence length. + pub fn bound(&self) -> usize { + self.bound + } + + /// Returns the number of input strings. + pub fn num_strings(&self) -> usize { + self.strings.len() + } + + /// Returns the total length of all input strings. + pub fn total_length(&self) -> usize { + self.strings.iter().map(|s| s.len()).sum() + } +} + +/// Check whether `needle` is a subsequence of `haystack` using greedy +/// left-to-right matching. +fn is_subsequence(needle: &[usize], haystack: &[usize]) -> bool { + let mut it = haystack.iter(); + for &ch in needle { + loop { + match it.next() { + Some(&c) if c == ch => break, + Some(_) => continue, + None => return false, + } + } + } + true +} + +impl Problem for ShortestCommonSupersequence { + const NAME: &'static str = "ShortestCommonSupersequence"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.alphabet_size; self.bound] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.bound { + return false; + } + if config.iter().any(|&v| v >= self.alphabet_size) { + return false; + } + self.strings.iter().all(|s| is_subsequence(s, config)) + } +} + +impl SatisfactionProblem for ShortestCommonSupersequence {} + +crate::declare_variants! { + ShortestCommonSupersequence => "alphabet_size ^ bound", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/shortest_common_supersequence.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 4300b2138..b22cc2a1c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -12,10 +12,13 @@ pub mod set; pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ - BicliqueCover, GraphPartitioning, HamiltonianPath, KColoring, MaxCut, MaximalIS, MaximumClique, - MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, - MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles, - RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, + BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, KColoring, MaxCut, + MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, + MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, + PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, +}; +pub use misc::{ + BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop, + ShortestCommonSupersequence, SubsetSum, }; -pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs new file mode 100644 index 000000000..8b13ac344 --- /dev/null +++ b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs @@ -0,0 +1,173 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::Problem; + +#[test] +fn test_isomorphicspanningtree_basic() { + // Triangle graph, path tree + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + assert_eq!(problem.dims(), vec![3, 3, 3]); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_graph_edges(), 3); + assert_eq!(problem.num_tree_edges(), 2); + assert_eq!(IsomorphicSpanningTree::NAME, "IsomorphicSpanningTree"); +} + +#[test] +fn test_isomorphicspanningtree_evaluation_yes() { + // Host graph: 0-1, 1-2, 0-2 (triangle) + // Tree: 0-1, 1-2 (path) + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + // Identity mapping: π = [0, 1, 2] + // Tree edges: (0,1) -> (0,1) ✓, (1,2) -> (1,2) ✓ + assert!(problem.evaluate(&[0, 1, 2])); + + // Reversed: π = [2, 1, 0] + // Tree edges: (0,1) -> (2,1) ✓, (1,2) -> (1,0) ✓ + assert!(problem.evaluate(&[2, 1, 0])); +} + +#[test] +fn test_isomorphicspanningtree_evaluation_no() { + // Host graph: path 0-1-2 (no edge 0-2) + // Tree: star with center 1: edges (0,1), (1,2) -- this is also a path, same structure + // Actually let's make a case where it fails: + // Host graph: 0-1, 2-3 (disconnected, 2 components) + // But wait, the tree must span, so let's use a connected graph where the tree doesn't fit. + + // Host graph: path 0-1-2-3 (edges: 0-1, 1-2, 2-3) + // Tree: star K_{1,3} center=0, leaves=1,2,3 (edges: 0-1, 0-2, 0-3) + // No vertex in graph has degree 3, so no valid mapping exists + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + // No permutation should work + assert!(!problem.evaluate(&[0, 1, 2, 3])); + assert!(!problem.evaluate(&[1, 0, 2, 3])); + assert!(!problem.evaluate(&[2, 1, 0, 3])); +} + +#[test] +fn test_isomorphicspanningtree_invalid_configs() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + // Not a permutation: repeated value + assert!(!problem.evaluate(&[0, 0, 1])); + // Out of range + assert!(!problem.evaluate(&[0, 1, 3])); + // Wrong length + assert!(!problem.evaluate(&[0, 1])); +} + +#[test] +fn test_isomorphicspanningtree_solver_yes() { + // Complete graph K4, any tree with 4 vertices should have a solution + 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), (1, 2), (2, 3)]); // path + let problem = IsomorphicSpanningTree::new(graph, tree); + + let solver = BruteForce::new(); + let sol = solver.find_satisfying(&problem); + assert!(sol.is_some()); + assert!(problem.evaluate(&sol.unwrap())); + + // All satisfying solutions should be valid + let all = solver.find_all_satisfying(&problem); + assert!(!all.is_empty()); + for s in &all { + assert!(problem.evaluate(s)); + } +} + +#[test] +fn test_isomorphicspanningtree_solver_no() { + // Path graph 0-1-2-3, star tree K_{1,3} + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + let solver = BruteForce::new(); + let sol = solver.find_satisfying(&problem); + assert!(sol.is_none()); + + let all = solver.find_all_satisfying(&problem); + assert!(all.is_empty()); +} + +#[test] +fn test_isomorphicspanningtree_serialization() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: IsomorphicSpanningTree = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.num_vertices(), 3); + assert_eq!(deserialized.num_graph_edges(), 3); + assert_eq!(deserialized.num_tree_edges(), 2); + // Verify same evaluation + assert!(deserialized.evaluate(&[0, 1, 2])); +} + +#[test] +fn test_isomorphicspanningtree_caterpillar_example() { + // Example from the issue: 7-vertex graph with caterpillar tree + let graph = SimpleGraph::new( + 7, + vec![ + (0, 1), + (0, 2), + (0, 3), + (1, 2), + (1, 4), + (2, 3), + (2, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 6), + (1, 3), + ], + ); + // Caterpillar tree: a-b, b-c, c-d, d-e, b-f, c-g + // Using vertex indices: 0-1, 1-2, 2-3, 3-4, 1-5, 2-6 + let tree = SimpleGraph::new(7, vec![(0, 1), (1, 2), (2, 3), (3, 4), (1, 5), (2, 6)]); + let problem = IsomorphicSpanningTree::new(graph, tree); + + // The issue gives solution: a→0, b→1, c→2, d→3, e→6, f→4, g→5 + // As config: π = [0, 1, 2, 3, 6, 4, 5] + assert!(problem.evaluate(&[0, 1, 2, 3, 6, 4, 5])); +} + +#[test] +fn test_isomorphicspanningtree_variant() { + assert!(IsomorphicSpanningTree::variant().is_empty()); +} + +#[test] +#[should_panic(expected = "graph and tree must have the same number of vertices")] +fn test_isomorphicspanningtree_mismatched_sizes() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + IsomorphicSpanningTree::new(graph, tree); +} + +#[test] +#[should_panic(expected = "tree must have exactly n-1 edges")] +fn test_isomorphicspanningtree_not_a_tree() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + // Not a tree: 3 edges for 3 vertices (has a cycle) + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + IsomorphicSpanningTree::new(graph, tree); +} diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index c92b5348f..f6055d1d3 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -224,7 +224,8 @@ fn test_min_sum_multicenter_serialization() { let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); let json = serde_json::to_string(&problem).unwrap(); - let deserialized: MinimumSumMulticenter = serde_json::from_str(&json).unwrap(); + let deserialized: MinimumSumMulticenter = + serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.graph().num_vertices(), 3); assert_eq!(deserialized.graph().num_edges(), 2); diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs new file mode 100644 index 000000000..fe468bd89 --- /dev/null +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -0,0 +1,161 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_flow_shop_scheduling_creation() { + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + ); + assert_eq!(problem.num_jobs(), 5); + assert_eq!(problem.num_processors(), 3); + assert_eq!(problem.deadline(), 25); + assert_eq!(problem.dims().len(), 5); + // Lehmer code encoding: dims = [5, 4, 3, 2, 1] + assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); +} + +#[test] +fn test_flow_shop_scheduling_evaluate_feasible() { + // From issue example: 3 machines, 5 jobs + // Job 0: [3, 4, 2], Job 1: [2, 3, 5], Job 2: [4, 1, 3], Job 3: [1, 5, 4], Job 4: [3, 2, 3] + // Sequence j4, j1, j5, j3, j2 = jobs [3, 0, 4, 2, 1] (0-indexed) + // This has makespan 23 <= 25 + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + ); + + // Lehmer code for job_order [3, 0, 4, 2, 1]: + // available=[0,1,2,3,4], pick 3 -> idx 3; available=[0,1,2,4], pick 0 -> idx 0; + // available=[1,2,4], pick 4 -> idx 2; available=[1,2], pick 2 -> idx 1; + // available=[1], pick 1 -> idx 0 + let config = vec![3, 0, 2, 1, 0]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_evaluate_infeasible() { + // Same instance, deadline of 15 (below the best makespan of 23) + let problem = FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 15, // Very tight deadline, likely infeasible + ); + + // The sequence j4,j1,j5,j3,j2 gives makespan 23 > 15 + // Lehmer code for job_order [3, 0, 4, 2, 1] = [3, 0, 2, 1, 0] + let config = vec![3, 0, 2, 1, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_invalid_config() { + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10); + + // Lehmer code out of range: dims = [2, 1], so config[0] must be < 2, config[1] must be < 1 + assert!(!problem.evaluate(&[2, 0])); // config[0] = 2 >= 2 + assert!(!problem.evaluate(&[0, 1])); // config[1] = 1 >= 1 + // Wrong length + assert!(!problem.evaluate(&[0])); + assert!(!problem.evaluate(&[0, 0, 0])); +} + +#[test] +fn test_flow_shop_scheduling_problem_name() { + assert_eq!(::NAME, "FlowShopScheduling"); +} + +#[test] +fn test_flow_shop_scheduling_variant() { + let v = ::variant(); + assert!(v.is_empty()); +} + +#[test] +fn test_flow_shop_scheduling_serialization() { + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4], vec![2, 1]], 10); + let json = serde_json::to_value(&problem).unwrap(); + let restored: FlowShopScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.task_lengths(), problem.task_lengths()); + assert_eq!(restored.deadline(), problem.deadline()); +} + +#[test] +fn test_flow_shop_scheduling_compute_makespan() { + // 2 machines, 3 jobs + // Job 0: [3, 2], Job 1: [2, 4], Job 2: [1, 3] + let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); + + // Order: job 0, job 1, job 2 + // Machine 0: j0[0,3], j1[3,5], j2[5,6] + // Machine 1: j0[3,5], j1[5,9], j2[9,12] + // Makespan = 12 + assert_eq!(problem.compute_makespan(&[0, 1, 2]), 12); +} + +#[test] +fn test_flow_shop_scheduling_brute_force_solver() { + // Small instance: 2 machines, 3 jobs, generous deadline + let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let config = solution.unwrap(); + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_flow_shop_scheduling_brute_force_unsatisfiable() { + // 2 machines, 2 jobs with impossible deadline + // Job 0: [5, 5], Job 1: [5, 5] + // Best makespan: min of two orders: + // [0,1]: M0: 0-5, 5-10; M1: 5-10, 10-15 -> 15 + // [1,0]: same by symmetry -> 15 + // Deadline 10 < 15 => unsatisfiable + let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_flow_shop_scheduling_empty() { + let problem = FlowShopScheduling::new(3, vec![], 0); + assert_eq!(problem.num_jobs(), 0); + assert_eq!(problem.dims(), Vec::::new()); + // Empty config should be satisfying (no jobs to schedule) + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_flow_shop_scheduling_single_job() { + // 3 machines, 1 job: [2, 3, 4] + // Makespan = 2 + 3 + 4 = 9 + let problem = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 10); + assert!(problem.evaluate(&[0])); // makespan 9 <= 10 + let tight = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 8); + assert!(!tight.evaluate(&[0])); // makespan 9 > 8 +} diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs new file mode 100644 index 000000000..00fe116e7 --- /dev/null +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -0,0 +1,114 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_shortestcommonsupersequence_basic() { + let problem = ShortestCommonSupersequence::new( + 3, + vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], + 7, + ); + assert_eq!(problem.alphabet_size(), 3); + assert_eq!(problem.num_strings(), 3); + assert_eq!(problem.bound(), 7); + assert_eq!(problem.total_length(), 12); + assert_eq!(problem.dims(), vec![3; 7]); + assert_eq!( + ::NAME, + "ShortestCommonSupersequence" + ); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_shortestcommonsupersequence_evaluate_yes() { + // alphabet {a=0, b=1, c=2} + // strings: [0,1,2,1] "abcb", [1,2,0,1] "bcab", [0,2,1,0] "acba" + // supersequence config [0,1,2,0,2,1,0] = "abcacba" + let problem = ShortestCommonSupersequence::new( + 3, + vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], + 7, + ); + // [0,1,2,1] matches at positions 0,1,2,5 + // [1,2,0,1] matches at positions 1,2,3,5 + // [0,2,1,0] matches at positions 0,2,5,6 + assert!(problem.evaluate(&[0, 1, 2, 0, 2, 1, 0])); +} + +#[test] +fn test_shortestcommonsupersequence_evaluate_no() { + let problem = ShortestCommonSupersequence::new( + 3, + vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], + 7, + ); + // [0,0,0,0,0,0,0] cannot contain [0,1,2,1] as subsequence + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_shortestcommonsupersequence_out_of_range() { + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]], 3); + // value 2 is out of range for alphabet_size=2 + assert!(!problem.evaluate(&[0, 2, 1])); +} + +#[test] +fn test_shortestcommonsupersequence_wrong_length() { + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]], 3); + // too short + assert!(!problem.evaluate(&[0, 1])); + // too long + assert!(!problem.evaluate(&[0, 1, 0, 1])); +} + +#[test] +fn test_shortestcommonsupersequence_brute_force() { + // alphabet {0,1}, strings [0,1] and [1,0], bound 3 + // e.g. [0,1,0] or [1,0,1] should work + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_shortestcommonsupersequence_empty_instance() { + // No strings, bound 0: vacuously satisfied on empty config + let problem = ShortestCommonSupersequence::new(2, vec![], 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_shortestcommonsupersequence_unsatisfiable() { + // strings [0,1] and [1,0] over binary alphabet, bound 2: impossible + // Any length-2 binary string is either "00","01","10","11" + // "01" contains [0,1] but not [1,0]; "10" contains [1,0] but not [0,1] + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 2); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_shortestcommonsupersequence_single_string() { + // Single string [0,1,2] over ternary alphabet, bound 3: the string itself is a solution + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2]], 3); + assert!(problem.evaluate(&[0, 1, 2])); + // A different string that doesn't contain [0,1,2] as subsequence + assert!(!problem.evaluate(&[2, 1, 0])); +} + +#[test] +fn test_shortestcommonsupersequence_serialization() { + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]], 5); + let json = serde_json::to_value(&problem).unwrap(); + let restored: ShortestCommonSupersequence = serde_json::from_value(json).unwrap(); + assert_eq!(restored.alphabet_size(), problem.alphabet_size()); + assert_eq!(restored.strings(), problem.strings()); + assert_eq!(restored.bound(), problem.bound()); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 91754a81d..20fea88a0 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -16,8 +16,8 @@ fn check_problem_trait(problem: &P, name: &str) { ); for d in &dims { assert!( - *d >= 2, - "{} should have at least 2 choices per dimension", + *d >= 1, + "{} should have at least 1 choice per dimension", name ); } @@ -91,13 +91,33 @@ fn test_all_problems_implement_trait_correctly() { "MinimumFeedbackArcSet", ); check_problem_trait( - &MinimumSumMulticenter::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3], vec![1i32; 2], 1), + &MinimumSumMulticenter::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![1i32; 3], + vec![1i32; 2], + 1, + ), "MinimumSumMulticenter", ); check_problem_trait( &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), "HamiltonianPath", ); + check_problem_trait( + &IsomorphicSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + ), + "IsomorphicSpanningTree", + ); + check_problem_trait( + &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3), + "ShortestCommonSupersequence", + ); + check_problem_trait( + &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), + "FlowShopScheduling", + ); } #[test] @@ -148,7 +168,13 @@ fn test_direction() { Direction::Minimize ); assert_eq!( - MinimumSumMulticenter::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3], vec![1i32; 2], 1).direction(), + MinimumSumMulticenter::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![1i32; 3], + vec![1i32; 2], + 1 + ) + .direction(), Direction::Minimize );