From 9e05f402e993294132ca37302c9c350cdae20c9f Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:10:56 +0000 Subject: [PATCH 1/9] Add plan for #412: ShortestCommonSupersequence model --- ...026-03-13-shortest-common-supersequence.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/plans/2026-03-13-shortest-common-supersequence.md diff --git a/docs/plans/2026-03-13-shortest-common-supersequence.md b/docs/plans/2026-03-13-shortest-common-supersequence.md new file mode 100644 index 000000000..876cb7d5e --- /dev/null +++ b/docs/plans/2026-03-13-shortest-common-supersequence.md @@ -0,0 +1,82 @@ +# Plan: Add ShortestCommonSupersequence Model (#412) + +## Overview +Add the Shortest Common Supersequence (SCS) satisfaction problem to the codebase. SCS asks: given a set of strings R over alphabet Sigma and a bound K, does there exist a string w with |w| <= K such that every string in R is a subsequence of w? + +## Information Checklist +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `ShortestCommonSupersequence` | +| 2 | Mathematical definition | Given alphabet Sigma, strings R, bound K: does w exist with \|w\| <= K containing all strings in R as subsequences? | +| 3 | Problem type | Satisfaction (decision) | +| 4 | Type parameters | None | +| 5 | Struct fields | `alphabet_size: usize`, `strings: Vec>`, `bound: usize` | +| 6 | Configuration space | `vec![alphabet_size; bound]` — each position picks an alphabet symbol | +| 7 | Feasibility check | Each string in R must be a subsequence of the constructed supersequence w | +| 8 | Objective function | `bool` — true iff all strings are subsequences of w | +| 9 | Best known exact | Brute force O(alphabet_size^bound); for 2 strings O(\|x1\|*\|x2\|) via LCS duality | +| 10 | Solving strategy | BruteForce (enumerate all strings of length bound over alphabet) | +| 11 | Category | `misc` (string sequences, unique input structure) | + +## Size Getters +- `alphabet_size()` -> usize (|Sigma|) +- `num_strings()` -> usize (|R|) +- `bound()` -> usize (K) +- `total_length()` -> usize (sum of |x| for x in R) + +## Complexity String +`"alphabet_size ^ bound"` — brute-force enumeration of all candidate strings. + +## Steps + +### Step 1: Implement the model +**File:** `src/models/misc/shortest_common_supersequence.rs` + +Following `SubsetSum` as the reference satisfaction problem: +1. `inventory::submit!` with ProblemSchemaEntry (fields: `alphabet_size`, `strings`, `bound`) +2. Struct with `#[derive(Debug, Clone, Serialize, Deserialize)]` +3. Constructor `new(alphabet_size: usize, strings: Vec>, bound: usize)` +4. Accessor methods: `alphabet_size()`, `strings()`, `bound()`, `num_strings()`, `total_length()` +5. `Problem` trait: NAME = "ShortestCommonSupersequence", Metric = bool +6. `dims()` returns `vec![alphabet_size; bound]` +7. `evaluate()`: construct w from config, check each string in R is a subsequence of w +8. `SatisfactionProblem` marker trait impl +9. `declare_variants! { ShortestCommonSupersequence => "alphabet_size ^ bound" }` +10. `#[cfg(test)] #[path]` link to test file + +**Subsequence check logic:** For each string s in R, greedily match characters left-to-right in w. If all characters of s are matched, s is a subsequence. All strings must be subsequences. + +**Config interpretation:** config[i] is the alphabet index at position i of w. If config[i] >= alphabet_size, evaluate returns false (out-of-range). + +### Step 2: Register the model +1. `src/models/misc/mod.rs` — add `pub(crate) mod shortest_common_supersequence;` and `pub use shortest_common_supersequence::ShortestCommonSupersequence;` +2. `src/models/mod.rs` — add `ShortestCommonSupersequence` to misc re-export + +### Step 3: Register in CLI +1. `problemreductions-cli/src/dispatch.rs`: + - `load_problem()`: add `"ShortestCommonSupersequence" => deser_sat::(data)` + - `serialize_any_problem()`: add `"ShortestCommonSupersequence" => try_ser::(any)` +2. `problemreductions-cli/src/problem_name.rs`: + - `resolve_alias()`: add `"shortestcommonsupersequence" => "ShortestCommonSupersequence"` and `"scs" => "ShortestCommonSupersequence"` +3. `problemreductions-cli/src/commands/create.rs`: + - Add match arm for "ShortestCommonSupersequence" that parses `--strings` (semicolon-separated sequences of alphabet indices) and `--bound` +4. `problemreductions-cli/src/cli.rs`: + - Add `--strings` flag if not already present, update `all_data_flags_empty()` and help table + +### Step 4: Write unit tests +**File:** `src/unit_tests/models/misc/shortest_common_supersequence.rs` + +Tests: +- `test_shortestcommonsupersequence_basic` — construct instance, verify dims +- `test_shortestcommonsupersequence_evaluate_yes` — test satisfying config (from issue example 1) +- `test_shortestcommonsupersequence_evaluate_no` — test unsatisfying config +- `test_shortestcommonsupersequence_out_of_range` — config values >= alphabet_size return false +- `test_shortestcommonsupersequence_wrong_length` — wrong config length returns false +- `test_shortestcommonsupersequence_brute_force` — BruteForce solver finds solution for small instance +- `test_shortestcommonsupersequence_serialization` — serde round-trip + +### Step 5: Document in paper +Invoke `/write-model-in-paper` for ShortestCommonSupersequence. + +### Step 6: Verify +Run `make check` (fmt + clippy + test). Fix any issues. From 9408aab9aa327107ac8cb829711ad36afb0092ff Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:16:46 +0000 Subject: [PATCH 2/9] Implement #412: Add ShortestCommonSupersequence model --- docs/paper/reductions.typ | 61 ++++++++ docs/paper/references.bib | 22 +++ problemreductions-cli/src/cli.rs | 7 + problemreductions-cli/src/commands/create.rs | 44 +++++- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 2 + src/models/misc/mod.rs | 3 + .../misc/shortest_common_supersequence.rs | 140 ++++++++++++++++++ src/models/mod.rs | 2 +- .../misc/shortest_common_supersequence.rs | 91 ++++++++++++ 10 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 src/models/misc/shortest_common_supersequence.rs create mode 100644 src/unit_tests/models/misc/shortest_common_supersequence.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 565abdb14..abba6b1d2 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -55,6 +55,7 @@ "ClosestVectorProblem": [Closest Vector Problem], "SubsetSum": [Subset Sum], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "ShortestCommonSupersequence": [Shortest Common Supersequence], ) // Definition label: "def:" — each definition block must have a matching label @@ -988,6 +989,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$. +] + // 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 0ec874f61..8403d5114 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -456,3 +456,25 @@ @article{cygan2014 note = {Conference version: STOC 2014}, doi = {10.1137/140990255} } + +@article{maier1978, + author = {David Maier}, + title = {The Complexity of Some Problems on Subsequences and Supersequences}, + journal = {Journal of the ACM}, + volume = {25}, + number = {2}, + pages = {322--336}, + year = {1978}, + doi = {10.1145/322063.322075} +} + +@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} +} diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 91792e20b..e8aeeebc5 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -218,6 +218,7 @@ Flags by problem type: BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] FVS --arcs [--weights] [--num-vertices] + SCS --strings, --bound ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -332,6 +333,12 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Input strings for SCS (semicolon-separated, each string is comma-separated alphabet indices, e.g., "0,1,2;1,2,0") + #[arg(long)] + pub strings: Option, + /// Length bound for SCS + #[arg(long)] + pub bound: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2df4f0994..009483e21 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,7 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; -use problemreductions::models::misc::{BinPacking, PaintShop}; +use problemreductions::models::misc::{BinPacking, PaintShop, ShortestCommonSupersequence}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -48,6 +48,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.target_vec.is_none() && args.bounds.is_none() && args.arcs.is_none() + && args.strings.is_none() + && args.bound.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -87,6 +89,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "Factoring" => "--target 15 --m 4 --n 4", + "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", } } @@ -459,6 +462,45 @@ 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}" + ) + })?; + let strings: Vec> = strings_str + .split(';') + .map(|s| { + s.trim() + .split(',') + .map(|v| { + v.trim() + .parse::() + .map_err(|e| anyhow::anyhow!("Invalid alphabet index: {}", e)) + }) + .collect::>>() + }) + .collect::>>()?; + let alphabet_size = strings + .iter() + .flat_map(|s| s.iter()) + .copied() + .max() + .map(|m| m + 1) + .unwrap_or(0); + ( + ser(ShortestCommonSupersequence::new(alphabet_size, strings, bound))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackVertexSet "MinimumFeedbackVertexSet" => { let arcs_str = args.arcs.as_ref().ok_or_else(|| { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 49dd523cb..09bfeb962 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, SubsetSum}; +use problemreductions::models::misc::{BinPacking, Knapsack, ShortestCommonSupersequence, SubsetSum}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -248,6 +248,7 @@ pub fn load_problem( "Knapsack" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), "SubsetSum" => deser_sat::(data), + "ShortestCommonSupersequence" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -311,6 +312,7 @@ pub fn serialize_any_problem( "Knapsack" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), "SubsetSum" => try_ser::(any), + "ShortestCommonSupersequence" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2b6c8c737..5d05063c7 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -22,6 +22,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("CVP", "ClosestVectorProblem"), ("MaxMatching", "MaximumMatching"), ("FVS", "MinimumFeedbackVertexSet"), + ("SCS", "ShortestCommonSupersequence"), ]; /// Resolve a short alias to the canonical problem name. @@ -56,6 +57,7 @@ pub fn resolve_alias(input: &str) -> String { "knapsack" => "Knapsack".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), "subsetsum" => "SubsetSum".to_string(), + "scs" | "shortestcommonsupersequence" => "ShortestCommonSupersequence".to_string(), _ => input.to_string(), // pass-through for exact names } } diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 36ebe905b..7d7a441d9 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -5,16 +5,19 @@ //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`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 knapsack; pub(crate) mod paintshop; +pub(crate) mod shortest_common_supersequence; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; 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..28dfba099 --- /dev/null +++ b/src/models/misc/shortest_common_supersequence.rs @@ -0,0 +1,140 @@ +//! 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`). 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: "Upper bound on supersequence length" }, + ], + } +} + +/// 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 `B` such +/// that every input string is a subsequence of `w`. +/// +/// # 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. + pub fn new(alphabet_size: usize, strings: Vec>, bound: usize) -> Self { + 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 ceb584ce2..2d84b3a20 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,5 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, ShortestCommonSupersequence, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; 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..675123c66 --- /dev/null +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -0,0 +1,91 @@ +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_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()); +} From e10e2d63f1fc7d1186b9a0027f49c615bb7d9f12 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:19:07 +0000 Subject: [PATCH 3/9] style: apply rustfmt formatting --- .../maximumindependentset_to_maximumclique.json | 8 ++++---- examples/detect_unreachable_from_3sat.rs | 13 +++---------- examples/reduction_binpacking_to_ilp.rs | 14 ++++++-------- problemreductions-cli/src/commands/create.rs | 14 +++++++------- problemreductions-cli/src/dispatch.rs | 4 +++- src/models/mod.rs | 4 +++- .../models/misc/shortest_common_supersequence.rs | 6 +----- 7 files changed, 27 insertions(+), 36 deletions(-) diff --git a/docs/paper/examples/maximumindependentset_to_maximumclique.json b/docs/paper/examples/maximumindependentset_to_maximumclique.json index 21a8853eb..abcd99576 100644 --- a/docs/paper/examples/maximumindependentset_to_maximumclique.json +++ b/docs/paper/examples/maximumindependentset_to_maximumclique.json @@ -2,8 +2,8 @@ "source": { "problem": "MaximumIndependentSet", "variant": { - "graph": "SimpleGraph", - "weight": "i32" + "weight": "i32", + "graph": "SimpleGraph" }, "instance": { "edges": [ @@ -31,8 +31,8 @@ "target": { "problem": "MaximumClique", "variant": { - "weight": "i32", - "graph": "SimpleGraph" + "graph": "SimpleGraph", + "weight": "i32" }, "instance": { "edges": [ diff --git a/examples/detect_unreachable_from_3sat.rs b/examples/detect_unreachable_from_3sat.rs index c48672276..b770ae6e9 100644 --- a/examples/detect_unreachable_from_3sat.rs +++ b/examples/detect_unreachable_from_3sat.rs @@ -109,8 +109,7 @@ fn main() { // Check if ALL variants of this problem are P-time // (conservative: if any variant could be hard, don't classify as P) let variants = graph.variants_for(name); - variants.len() == 1 - && variants[0].get(*key).map(|s| s.as_str()) == Some(*val) + variants.len() == 1 && variants[0].get(*key).map(|s| s.as_str()) == Some(*val) } } }); @@ -143,10 +142,7 @@ fn main() { } if !p_time.is_empty() { - println!( - "In P — correctly unreachable ({}):", - p_time.len() - ); + println!("In P — correctly unreachable ({}):", p_time.len()); for name in &p_time { println!(" {name}"); } @@ -165,10 +161,7 @@ fn main() { } if !orphans.is_empty() { - println!( - "Orphans — no reductions at all ({}):", - orphans.len() - ); + println!("Orphans — no reductions at all ({}):", orphans.len()); for name in &orphans { println!(" {name}"); } diff --git a/examples/reduction_binpacking_to_ilp.rs b/examples/reduction_binpacking_to_ilp.rs index 104d707a4..74b87e1bd 100644 --- a/examples/reduction_binpacking_to_ilp.rs +++ b/examples/reduction_binpacking_to_ilp.rs @@ -53,7 +53,10 @@ pub fn run() { // 5. Extract source solution let bp_solution = reduction.extract_solution(&ilp_solution); - println!("Source BinPacking solution (bin assignments): {:?}", bp_solution); + println!( + "Source BinPacking solution (bin assignments): {:?}", + bp_solution + ); // 6. Verify let size = bp.evaluate(&bp_solution); @@ -76,13 +79,8 @@ pub fn run() { let source_variant = variant_to_map(BinPacking::::variant()); let target_variant = variant_to_map(ILP::::variant()); - let overhead = lookup_overhead( - "BinPacking", - &source_variant, - "ILP", - &target_variant, - ) - .unwrap_or_default(); + let overhead = + lookup_overhead("BinPacking", &source_variant, "ILP", &target_variant).unwrap_or_default(); let data = ReductionData { source: ProblemSide { diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 009483e21..e40ebf840 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -466,14 +466,10 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "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}" - ) + anyhow::anyhow!("ShortestCommonSupersequence requires --strings\n\n{usage}") })?; let bound = args.bound.ok_or_else(|| { - anyhow::anyhow!( - "ShortestCommonSupersequence requires --bound\n\n{usage}" - ) + anyhow::anyhow!("ShortestCommonSupersequence requires --bound\n\n{usage}") })?; let strings: Vec> = strings_str .split(';') @@ -496,7 +492,11 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { .map(|m| m + 1) .unwrap_or(0); ( - ser(ShortestCommonSupersequence::new(alphabet_size, strings, bound))?, + ser(ShortestCommonSupersequence::new( + alphabet_size, + strings, + bound, + ))?, resolved_variant.clone(), ) } diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 09bfeb962..082df9bfd 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, ShortestCommonSupersequence, SubsetSum}; +use problemreductions::models::misc::{ + BinPacking, Knapsack, ShortestCommonSupersequence, SubsetSum, +}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; diff --git a/src/models/mod.rs b/src/models/mod.rs index 2d84b3a20..abd61cb8e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,7 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, ShortestCommonSupersequence, SubsetSum}; +pub use misc::{ + BinPacking, Factoring, Knapsack, PaintShop, ShortestCommonSupersequence, SubsetSum, +}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 675123c66..4a2cb72d4 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -78,11 +78,7 @@ fn test_shortestcommonsupersequence_brute_force() { #[test] fn test_shortestcommonsupersequence_serialization() { - let problem = ShortestCommonSupersequence::new( - 3, - vec![vec![0, 1, 2], vec![2, 1, 0]], - 5, - ); + 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()); From 9e074537a1750d762a1dc3172a74a71dc305e462 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:21:05 +0000 Subject: [PATCH 4/9] Address review: fix docs, add --alphabet-size flag, add edge case tests --- problemreductions-cli/src/cli.rs | 3 +++ problemreductions-cli/src/commands/create.rs | 11 +++++++- .../misc/shortest_common_supersequence.rs | 2 +- .../misc/shortest_common_supersequence.rs | 27 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e8aeeebc5..acbd49125 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -339,6 +339,9 @@ pub struct CreateArgs { /// Length bound for SCS #[arg(long)] pub bound: 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 e40ebf840..0f7567e5a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -50,6 +50,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.arcs.is_none() && args.strings.is_none() && args.bound.is_none() + && args.alphabet_size.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -484,13 +485,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { .collect::>>() }) .collect::>>()?; - let alphabet_size = strings + 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, diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index 28dfba099..f288fea9c 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -1,7 +1,7 @@ //! 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 +//! whether there exists a common supersequence of length exactly `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`). This problem is //! NP-hard (Maier, 1978). diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 4a2cb72d4..00fe116e7 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -76,6 +76,33 @@ fn test_shortestcommonsupersequence_brute_force() { 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); From f744726164ad269a55c083e15265d22b326a7db5 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:21:09 +0000 Subject: [PATCH 5/9] chore: remove plan file after implementation --- ...026-03-13-shortest-common-supersequence.md | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 docs/plans/2026-03-13-shortest-common-supersequence.md diff --git a/docs/plans/2026-03-13-shortest-common-supersequence.md b/docs/plans/2026-03-13-shortest-common-supersequence.md deleted file mode 100644 index 876cb7d5e..000000000 --- a/docs/plans/2026-03-13-shortest-common-supersequence.md +++ /dev/null @@ -1,82 +0,0 @@ -# Plan: Add ShortestCommonSupersequence Model (#412) - -## Overview -Add the Shortest Common Supersequence (SCS) satisfaction problem to the codebase. SCS asks: given a set of strings R over alphabet Sigma and a bound K, does there exist a string w with |w| <= K such that every string in R is a subsequence of w? - -## Information Checklist -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `ShortestCommonSupersequence` | -| 2 | Mathematical definition | Given alphabet Sigma, strings R, bound K: does w exist with \|w\| <= K containing all strings in R as subsequences? | -| 3 | Problem type | Satisfaction (decision) | -| 4 | Type parameters | None | -| 5 | Struct fields | `alphabet_size: usize`, `strings: Vec>`, `bound: usize` | -| 6 | Configuration space | `vec![alphabet_size; bound]` — each position picks an alphabet symbol | -| 7 | Feasibility check | Each string in R must be a subsequence of the constructed supersequence w | -| 8 | Objective function | `bool` — true iff all strings are subsequences of w | -| 9 | Best known exact | Brute force O(alphabet_size^bound); for 2 strings O(\|x1\|*\|x2\|) via LCS duality | -| 10 | Solving strategy | BruteForce (enumerate all strings of length bound over alphabet) | -| 11 | Category | `misc` (string sequences, unique input structure) | - -## Size Getters -- `alphabet_size()` -> usize (|Sigma|) -- `num_strings()` -> usize (|R|) -- `bound()` -> usize (K) -- `total_length()` -> usize (sum of |x| for x in R) - -## Complexity String -`"alphabet_size ^ bound"` — brute-force enumeration of all candidate strings. - -## Steps - -### Step 1: Implement the model -**File:** `src/models/misc/shortest_common_supersequence.rs` - -Following `SubsetSum` as the reference satisfaction problem: -1. `inventory::submit!` with ProblemSchemaEntry (fields: `alphabet_size`, `strings`, `bound`) -2. Struct with `#[derive(Debug, Clone, Serialize, Deserialize)]` -3. Constructor `new(alphabet_size: usize, strings: Vec>, bound: usize)` -4. Accessor methods: `alphabet_size()`, `strings()`, `bound()`, `num_strings()`, `total_length()` -5. `Problem` trait: NAME = "ShortestCommonSupersequence", Metric = bool -6. `dims()` returns `vec![alphabet_size; bound]` -7. `evaluate()`: construct w from config, check each string in R is a subsequence of w -8. `SatisfactionProblem` marker trait impl -9. `declare_variants! { ShortestCommonSupersequence => "alphabet_size ^ bound" }` -10. `#[cfg(test)] #[path]` link to test file - -**Subsequence check logic:** For each string s in R, greedily match characters left-to-right in w. If all characters of s are matched, s is a subsequence. All strings must be subsequences. - -**Config interpretation:** config[i] is the alphabet index at position i of w. If config[i] >= alphabet_size, evaluate returns false (out-of-range). - -### Step 2: Register the model -1. `src/models/misc/mod.rs` — add `pub(crate) mod shortest_common_supersequence;` and `pub use shortest_common_supersequence::ShortestCommonSupersequence;` -2. `src/models/mod.rs` — add `ShortestCommonSupersequence` to misc re-export - -### Step 3: Register in CLI -1. `problemreductions-cli/src/dispatch.rs`: - - `load_problem()`: add `"ShortestCommonSupersequence" => deser_sat::(data)` - - `serialize_any_problem()`: add `"ShortestCommonSupersequence" => try_ser::(any)` -2. `problemreductions-cli/src/problem_name.rs`: - - `resolve_alias()`: add `"shortestcommonsupersequence" => "ShortestCommonSupersequence"` and `"scs" => "ShortestCommonSupersequence"` -3. `problemreductions-cli/src/commands/create.rs`: - - Add match arm for "ShortestCommonSupersequence" that parses `--strings` (semicolon-separated sequences of alphabet indices) and `--bound` -4. `problemreductions-cli/src/cli.rs`: - - Add `--strings` flag if not already present, update `all_data_flags_empty()` and help table - -### Step 4: Write unit tests -**File:** `src/unit_tests/models/misc/shortest_common_supersequence.rs` - -Tests: -- `test_shortestcommonsupersequence_basic` — construct instance, verify dims -- `test_shortestcommonsupersequence_evaluate_yes` — test satisfying config (from issue example 1) -- `test_shortestcommonsupersequence_evaluate_no` — test unsatisfying config -- `test_shortestcommonsupersequence_out_of_range` — config values >= alphabet_size return false -- `test_shortestcommonsupersequence_wrong_length` — wrong config length returns false -- `test_shortestcommonsupersequence_brute_force` — BruteForce solver finds solution for small instance -- `test_shortestcommonsupersequence_serialization` — serde round-trip - -### Step 5: Document in paper -Invoke `/write-model-in-paper` for ShortestCommonSupersequence. - -### Step 6: Verify -Run `make check` (fmt + clippy + test). Fix any issues. From 2a65a528ebd66b34568024c09d3fde0d2649b4d3 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 16:00:01 +0000 Subject: [PATCH 6/9] Address Copilot review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align docs/schema wording: bound is exact config length, equivalent to |w| ≤ B via padding - Add alphabet_size > 0 validation in constructor - Handle empty string segments in --strings CLI parsing - Add --alphabet-size to CLI help text - Regenerate problem_schemas.json Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 32 +++++++++++++++++++ problemreductions-cli/src/cli.rs | 2 +- problemreductions-cli/src/commands/create.rs | 6 +++- .../misc/shortest_common_supersequence.rs | 26 +++++++++++---- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 793fcc1f1..35ed83fb3 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -382,6 +382,17 @@ } ] }, + { + "name": "PartitionIntoTriangles", + "description": "Partition vertices into triangles (K3 subgraphs)", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The underlying graph G=(V,E) with |V| divisible by 3" + } + ] + }, { "name": "QUBO", "description": "Minimize quadratic unconstrained binary objective", @@ -440,6 +451,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 013e16448..3c565c098 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -223,7 +223,7 @@ Flags by problem type: SubgraphIsomorphism --graph (host), --pattern (pattern) LCS --strings FVS --arcs [--weights] [--num-vertices] - SCS --strings, --bound + SCS --strings, --bound [--alphabet-size] ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 28a77343e..fe591ff93 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -622,7 +622,11 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let strings: Vec> = strings_str .split(';') .map(|s| { - s.trim() + let trimmed = s.trim(); + if trimmed.is_empty() { + return Ok(Vec::new()); + } + trimmed .split(',') .map(|v| { v.trim() diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index f288fea9c..a6da920f0 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -1,10 +1,14 @@ //! 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 exactly `B`. A string +//! 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`). This problem is -//! NP-hard (Maier, 1978). +//! 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}; @@ -18,7 +22,7 @@ inventory::submit! { 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: "Upper bound on supersequence length" }, + FieldInfo { name: "bound", type_name: "usize", description: "Bound on supersequence length (configuration has exactly this many symbols)" }, ], } } @@ -26,8 +30,9 @@ inventory::submit! { /// 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 `B` such -/// that every input string is a subsequence of `w`. +/// 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 /// @@ -56,7 +61,16 @@ pub struct ShortestCommonSupersequence { 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, From 6e3dd210e1accb98d80aca389fb6a273d48f68e7 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 16:07:32 +0000 Subject: [PATCH 7/9] Fix agentic test issues: add SCS to prelude, fix CLI duplicate fields - Add ShortestCommonSupersequence to prelude re-exports (was missing unlike all other misc models) - Fix duplicate CLI struct fields (strings, bound) by sharing between LCS/SCS and RuralPostman/SCS with i64 type and casts at usage sites Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/cli.rs | 12 +++--------- problemreductions-cli/src/commands/create.rs | 6 ++---- src/lib.rs | 3 ++- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 3c565c098..bfdb2d7c1 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -338,24 +338,18 @@ pub struct CreateArgs { /// 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, - /// Input strings for SCS (semicolon-separated, each string is comma-separated alphabet indices, e.g., "0,1,2;1,2,0") - #[arg(long)] - pub strings: Option, - /// Length bound for SCS - #[arg(long)] - pub bound: Option, /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index fe591ff93..649ecd026 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -54,8 +54,6 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.is_none() && args.strings.is_none() && args.arcs.is_none() - && args.strings.is_none() - && args.bound.is_none() && args.alphabet_size.is_none() } @@ -265,7 +263,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, @@ -618,7 +616,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { })?; 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| { diff --git a/src/lib.rs b/src/lib.rs index 88f2f02e7..de2b40b5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,7 +49,8 @@ pub mod prelude { PartitionIntoTriangles, RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ - BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum, + BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, + ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; From 20cb9b5214fb57ec50d2f0cb5e33f8c035e8b9d6 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 16:26:07 +0000 Subject: [PATCH 8/9] chore: trigger CI Co-Authored-By: Claude Opus 4.6 From 806b2b5ffe86c1c4c1d375d77cc3d0798756093e Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 16:38:19 +0000 Subject: [PATCH 9/9] style: apply rustfmt formatting Co-Authored-By: Claude Opus 4.6 --- .../examples/maximumindependentset_to_maximumclique.json | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/paper/examples/maximumindependentset_to_maximumclique.json b/docs/paper/examples/maximumindependentset_to_maximumclique.json index abcd99576..352ddeb68 100644 --- a/docs/paper/examples/maximumindependentset_to_maximumclique.json +++ b/docs/paper/examples/maximumindependentset_to_maximumclique.json @@ -2,8 +2,8 @@ "source": { "problem": "MaximumIndependentSet", "variant": { - "weight": "i32", - "graph": "SimpleGraph" + "graph": "SimpleGraph", + "weight": "i32" }, "instance": { "edges": [ diff --git a/src/lib.rs b/src/lib.rs index de2b40b5e..50d7b8991 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, - PartitionIntoTriangles, RuralPostman, TravelingSalesman, + MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, PartitionIntoTriangles, + RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop,