From f2a15aef2646a5e016c4baed6b37153de3fc853b Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 23:06:42 +0800 Subject: [PATCH 1/7] Add plan for #439: StringToStringCorrection --- .../2026-03-16-string-to-string-correction.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/plans/2026-03-16-string-to-string-correction.md diff --git a/docs/plans/2026-03-16-string-to-string-correction.md b/docs/plans/2026-03-16-string-to-string-correction.md new file mode 100644 index 000000000..5cd91ddd9 --- /dev/null +++ b/docs/plans/2026-03-16-string-to-string-correction.md @@ -0,0 +1,154 @@ +# Plan: Add StringToStringCorrection Model + +**Issue:** #439 [Model] StringToStringCorrection +**Skill:** add-model +**Date:** 2026-03-16 + +## Information Checklist + +| # | Item | Value | +|---|------|-------| +| 1 | Problem name | `StringToStringCorrection` | +| 2 | Mathematical definition | Given finite alphabet Sigma, two strings x,y in Sigma*, and positive integer K. Can y be derived from x by K or fewer operations of single symbol deletion or adjacent symbol interchange? | +| 3 | Problem type | Satisfaction (bool) | +| 4 | Type parameters | None | +| 5 | Struct fields | `alphabet_size: usize`, `source: Vec`, `target: Vec`, `bound_k: usize` | +| 6 | Configuration space | `vec![2 * source.len() + 1; bound_k]` — K operation slots, each in {0..2*source_length} where 0..source_length = delete at position i, source_length..2*source_length = swap positions i and i+1, 2*source_length = no-op | +| 7 | Feasibility check | Apply operations left-to-right to mutable copy of source; skip no-ops; return false if any delete/swap index is out of bounds for current intermediate string | +| 8 | Objective function | `bool` — true iff result equals target after all operations | +| 9 | Best known algorithm | Brute-force: O((2*source_length+1)^bound_k) — Wagner (1975) proved NP-completeness | +| 10 | Solving strategy | BruteForce only | +| 11 | Category | `misc/` | +| 12 | Expected outcome | source=[0,1,2,3,1,0], target=[0,1,3,2,1], bound_k=2, alphabet_size=4. Solution: swap(2,3) then delete(5) → answer YES. Minimum cost is exactly 2. | + +**Associated rules:** #453 [Rule] SET COVERING to STRING-TO-STRING CORRECTION + +## Batch 1: Implementation (Steps 1-5.5) + +### Step 1: Create model file + +Create `src/models/misc/string_to_string_correction.rs` following `ShortestCommonSupersequence` as reference. + +**Schema entry:** +```rust +inventory::submit! { + ProblemSchemaEntry { + name: "StringToStringCorrection", + display_name: "String-to-String Correction", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Derive target string from source using at most K deletions and adjacent swaps", + fields: &[ + FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the finite alphabet" }, + FieldInfo { name: "source", type_name: "Vec", description: "Source string (symbol indices)" }, + FieldInfo { name: "target", type_name: "Vec", description: "Target string (symbol indices)" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of operations allowed" }, + ], + } +} +``` + +**Struct:** +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringToStringCorrection { + alphabet_size: usize, + source: Vec, + target: Vec, + bound_k: usize, +} +``` + +**Constructor:** Validate that `alphabet_size > 0` when source or target is non-empty, and all symbols in source/target are < alphabet_size. + +**Getters:** `alphabet_size()`, `source()`, `target()`, `bound_k()`, `source_length()`, `target_length()`. + +**Problem trait:** +- `NAME = "StringToStringCorrection"` +- `type Metric = bool` +- `variant() -> crate::variant_params![]` +- `dims() -> vec![2 * self.source.len() + 1; self.bound_k]` +- `evaluate()`: Apply operations left-to-right to a mutable Vec copy of source. For each operation slot: + - value < current_len → delete at that position + - value >= current_len && value < current_len + (current_len - 1) → swap adjacent at position (value - current_len) and (value - current_len + 1) + - value == 2 * source.len() (the no-op sentinel) + - Any other value → return false (invalid operation for current string state) + - After all ops, return result == target + + **IMPORTANT encoding note:** The config space is fixed at `2 * source.len() + 1` per slot. But as the string gets shorter from deletions, valid operation indices change. Values that were valid initially may become out-of-bounds after deletions. The evaluate function must handle this dynamically: check each operation against the *current* intermediate string length, not the original source length. + +**SatisfactionProblem impl:** marker trait, no methods. + +**declare_variants!:** +```rust +crate::declare_variants! { + default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound_k", +} +``` + +### Step 2: Register the model + +1. `src/models/misc/mod.rs` — add `pub(crate) mod string_to_string_correction;` and `pub use string_to_string_correction::StringToStringCorrection;` +2. `src/models/mod.rs` — add `StringToStringCorrection` to the `misc` re-export line +3. Update module doc comment in `misc/mod.rs` + +### Step 3: Register for CLI discovery + +In `problemreductions-cli/src/problem_name.rs` — no short alias needed (no well-established abbreviation). + +### Step 4: Add CLI creation support + +In `problemreductions-cli/src/commands/create.rs`: +- Add a match arm for `"StringToStringCorrection"` that parses `--source-string`, `--target-string`, `--bound`, `--alphabet-size` +- Source and target strings are comma-separated symbol indices (e.g., "0,1,2,3,1,0") + +In `problemreductions-cli/src/cli.rs`: +- Add `--source-string` and `--target-string` flags to `CreateArgs` +- Add entry to "Flags by problem type" help table +- Update `all_data_flags_empty()` + +Add example hint in `example_hint()`. + +### Step 5: Add canonical model example + +In the model file, add `canonical_model_example_specs()` function (feature-gated with `example-db`): +- Use the issue's example: alphabet_size=4, source=[0,1,2,3,1,0], target=[0,1,3,2,1], bound_k=2 +- Sample config: the known solution (swap at position 2, then delete at position 5) + +Register in `src/models/misc/mod.rs` `canonical_model_example_specs()`. + +### Step 6: Write unit tests + +Create `src/unit_tests/models/misc/string_to_string_correction.rs`: + +- `test_string_to_string_correction_creation` — construct instance, verify dims +- `test_string_to_string_correction_evaluation` — verify evaluate() on the known solution and invalid configs +- `test_string_to_string_correction_serialization` — round-trip serde +- `test_string_to_string_correction_solver` — BruteForce finds satisfying solution +- `test_string_to_string_correction_paper_example` — verify paper example instance, evaluate expected solution, check all satisfying solutions count +- `test_string_to_string_correction_invalid_operations` — test out-of-bounds operations return false +- `test_string_to_string_correction_unsatisfiable` — test case where no solution exists within bound + +Link test file via `#[cfg(test)] #[path = "..."] mod tests;`. + +### Step 7: Add trait_consistency entry + +In `src/unit_tests/trait_consistency.rs`: +- Add `check_problem_trait(...)` call with a small StringToStringCorrection instance +- No `test_direction` entry needed (satisfaction problem) + +## Batch 2: Paper Documentation (Step 6 from add-model) + +### Step 8: Write paper entry + +In `docs/paper/reductions.typ`: + +1. Add display name: `"StringToStringCorrection": [String-to-String Correction]` + +2. Write `problem-def("StringToStringCorrection")`: + - **Definition:** Given finite alphabet $Sigma$, source string $x in Sigma^*$, target string $y in Sigma^*$, and positive integer $K$, determine whether $y$ can be derived from $x$ by a sequence of at most $K$ operations, where each operation is either a single-symbol deletion or an adjacent-symbol interchange. + - **Background:** Classical NP-complete problem from Garey & Johnson (A4 SR20). Wagner (1975) proved NP-completeness via transformation from Set Covering. The standard edit distance (insert, delete, change) is solvable in O(|x|*|y|) time by Wagner-Fischer (1974), but restricting to only deletions and adjacent swaps makes the problem NP-complete. + - **Example:** Use the issue example with a visualization showing the source string, the two operations (swap + delete), and the resulting target string. + +3. Run `make paper` to verify compilation. From b0cd7a606be339ab3da355e69da9511ff5c498ec Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 23:35:50 +0800 Subject: [PATCH 2/7] Implement #439: Add StringToStringCorrection model Add the String-to-String Correction satisfaction problem (Garey & Johnson SR20). Given source/target strings and bound K, determine if the target can be derived from the source using at most K deletions and adjacent swaps. - Model: src/models/misc/string_to_string_correction.rs - Tests: 11 unit tests + 1 doctest (creation, evaluation, solver, paper example) - CLI: --source-string, --target-string, --bound, --alphabet-size flags - Paper: problem-def with figure, Wagner 1974/1975 references - Example-db: canonical instance with 2 satisfying solutions --- docs/paper/reductions.typ | 67 ++++++ docs/paper/references.bib | 20 ++ docs/src/reductions/problem_schemas.json | 26 +++ docs/src/reductions/reduction_graph.json | 13 +- problemreductions-cli/src/cli.rs | 9 +- problemreductions-cli/src/commands/create.rs | 54 ++++- src/example_db/fixtures/examples.json | 1 + src/models/misc/mod.rs | 4 + .../misc/string_to_string_correction.rs | 219 ++++++++++++++++++ src/models/mod.rs | 3 +- .../misc/string_to_string_correction.rs | 133 +++++++++++ src/unit_tests/trait_consistency.rs | 4 + 12 files changed, 547 insertions(+), 6 deletions(-) create mode 100644 src/models/misc/string_to_string_correction.rs create mode 100644 src/unit_tests/models/misc/string_to_string_correction.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 76cddb448..663994593 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -109,6 +109,7 @@ "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], + "StringToStringCorrection": [String-to-String Correction], ) // Definition label: "def:" — each definition block must have a matching label @@ -1937,6 +1938,72 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("StringToStringCorrection") + let source = x.instance.source + let target = x.instance.target + let alpha-size = x.instance.alphabet_size + let bound-k = x.instance.bound_k + let n = source.len() + // Alphabet mapping: 0->a, 1->b, 2->c, 3->d + let alpha-map = range(alpha-size).map(i => str.from-unicode(97 + i)) + let fmt-str(s) = s.map(c => alpha-map.at(c)).join("") + let src-str = fmt-str(source) + let tgt-str = fmt-str(target) + // Use solution [8, 5]: swap(2,3) then delete(5) + let sol = x.optimal.at(1) + // Trace the operations + let after-swap = (source.at(0), source.at(1), source.at(3), source.at(2), source.at(4), source.at(5)) + let after-swap-str = after-swap.map(c => alpha-map.at(c)).join("") + [ + #problem-def("StringToStringCorrection")[ + Given a finite alphabet $Sigma$, a source string $x in Sigma^*$, a target string $y in Sigma^*$, and a positive integer $K$, determine whether $y$ can be derived from $x$ by a sequence of at most $K$ operations, where each operation is either a _single-symbol deletion_ (remove one character at a chosen position) or an _adjacent-symbol interchange_ (swap two neighboring characters). + ][ + A classical NP-complete problem listed as SR20 in Garey and Johnson @garey1979. #cite(, form: "prose") proved NP-completeness via transformation from Set Covering. The standard edit distance problem --- allowing insertion, deletion, and substitution --- is solvable in $O(|x| dot |y|)$ time by the Wagner--Fischer dynamic programming algorithm @wagner1974. However, restricting the operation set to only deletions and adjacent swaps makes the problem NP-complete for unbounded alphabets. When only adjacent swaps are allowed (no deletions), the problem reduces to counting inversions and is polynomial @wagner1975.#footnote[No algorithm improving on brute-force is known for the general swap-and-delete variant.] + + *Example.* Let $Sigma = {#alpha-map.join(", ")}$, source $x = #src-str$ (length #n), target $y = #tgt-str$ (length #target.len()), and $K = #bound-k$. + + #figure({ + let blue = graph-colors.at(0) + let red = rgb("#e15759") + let cell(ch, highlight: false, strike: false) = { + let fill = if highlight { blue.transparentize(70%) } else { white } + box(width: 0.55cm, height: 0.55cm, fill: fill, stroke: 0.5pt + luma(120), + align(center + horizon, text(9pt, weight: "bold", + if strike { text(fill: red, [#sym.times]) } else { ch }))) + } + align(center, stack(dir: ttb, spacing: 0.5cm, + // Step 0: source + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[$x: quad$])), + ..source.map(c => cell(alpha-map.at(c))), + ), + // Step 1: after swap at positions 2,3 + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(2,3)$: quad])), + ..after-swap.map(c => cell(alpha-map.at(c), highlight: c != source.at(after-swap.position(cc => cc == c)))), + ), + // Step 2: after delete at position 5 + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[del$(5)$: quad])), + ..target.map(c => cell(alpha-map.at(c))), + cell([], strike: true), + ), + // Result + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[$= y$: quad])), + ..target.map(c => cell(alpha-map.at(c), highlight: true)), + ), + )) + }, + caption: [String-to-String Correction: transforming $x = #src-str$ into $y = #tgt-str$ with $K = #bound-k$ operations. Step 1 swaps adjacent symbols at positions 2 and 3; step 2 deletes the symbol at position 5.], + ) + + The transformation uses exactly $K = #bound-k$ operations (1 swap + 1 deletion), which is the minimum: a single operation cannot account for both the transposition of two symbols and the removal of one. + ] + ] +} + #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$. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 6482b48a2..3b6fe7aee 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1,3 +1,23 @@ +@inproceedings{wagner1975, + author = {Robert A. Wagner}, + title = {On the Complexity of the Extended String-to-String Correction Problem}, + booktitle = {Proceedings of the 7th Annual ACM Symposium on Theory of Computing (STOC)}, + pages = {218--223}, + year = {1975}, + doi = {10.1145/800116.803771} +} + +@article{wagner1974, + author = {Robert A. Wagner and Michael J. Fischer}, + title = {The String-to-String Correction Problem}, + journal = {Journal of the ACM}, + volume = {21}, + number = {1}, + pages = {168--173}, + year = {1974}, + doi = {10.1145/321796.321811} +} + @article{juttner2018, author = {Alpár Jüttner and Péter Madarasi}, title = {VF2++ — An improved subgraph isomorphism algorithm}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index a69a12756..ae80461be 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -755,6 +755,32 @@ } ] }, + { + "name": "StringToStringCorrection", + "description": "Derive target string from source using at most K deletions and adjacent swaps", + "fields": [ + { + "name": "alphabet_size", + "type_name": "usize", + "description": "Size of the finite alphabet" + }, + { + "name": "source", + "type_name": "Vec", + "description": "Source string (symbol indices)" + }, + { + "name": "target", + "type_name": "Vec", + "description": "Target string (symbol indices)" + }, + { + "name": "bound_k", + "type_name": "usize", + "description": "Maximum number of operations allowed" + } + ] + }, { "name": "SubgraphIsomorphism", "description": "Determine if host graph G contains a subgraph isomorphic to pattern graph H", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 5429b350f..09cebea11 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -534,6 +534,13 @@ "doc_path": "models/graph/struct.SteinerTree.html", "complexity": "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2" }, + { + "name": "StringToStringCorrection", + "variant": {}, + "category": "misc", + "doc_path": "models/misc/struct.StringToStringCorrection.html", + "complexity": "(2 * source_length + 1) ^ bound_k" + }, { "name": "SubgraphIsomorphism", "variant": {}, @@ -763,7 +770,7 @@ }, { "source": 22, - "target": 61, + "target": 62, "overhead": [ { "field": "num_elements", @@ -1391,7 +1398,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 62, + "source": 63, "target": 13, "overhead": [ { @@ -1406,7 +1413,7 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 62, + "source": 63, "target": 51, "overhead": [ { diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 32d58428c..664ddcd78 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -247,6 +247,7 @@ Flags by problem type: FlowShopScheduling --task-lengths, --deadline [--num-processors] MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] SCS --strings, --bound [--alphabet-size] + StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size] D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 ILP, CircuitSAT (via reduction only) @@ -440,9 +441,15 @@ pub struct CreateArgs { /// Number of processors/machines for FlowShopScheduling #[arg(long)] pub num_processors: Option, - /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) + /// Alphabet size for SCS or StringToStringCorrection (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, + /// Source string for StringToStringCorrection (comma-separated symbol indices, e.g., "0,1,2,3") + #[arg(long)] + pub source_string: Option, + /// Target string for StringToStringCorrection (comma-separated symbol indices, e.g., "0,1,3,2") + #[arg(long)] + pub target_string: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index dc1aaca64..b402aab71 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -11,7 +11,7 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, ShortestCommonSupersequence, SubsetSum, + PaintShop, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -76,6 +76,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.deadline.is_none() && args.num_processors.is_none() && args.alphabet_size.is_none() + && args.source_string.is_none() + && args.target_string.is_none() && args.capacities.is_none() && args.source_1.is_none() && args.sink_1.is_none() @@ -295,6 +297,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", + "StringToStringCorrection" => { + "--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2" + } _ => "", } } @@ -1398,6 +1403,53 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // StringToStringCorrection + "StringToStringCorrection" => { + let usage = "Usage: pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"; + let source_str = args.source_string.as_deref().ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --source-string\n\n{usage}") + })?; + let target_str = args.target_string.as_deref().ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --target-string\n\n{usage}") + })?; + let bound_k = args.bound.ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --bound\n\n{usage}") + })? as usize; + let parse_symbols = |s: &str| -> Result> { + if s.trim().is_empty() { + return Ok(Vec::new()); + } + s.split(',') + .map(|v| v.trim().parse::().context("invalid symbol index")) + .collect() + }; + let source = parse_symbols(source_str)?; + let target = parse_symbols(target_str)?; + let inferred = source + .iter() + .chain(target.iter()) + .copied() + .max() + .map_or(0, |m| m + 1); + let alphabet_size = args.alphabet_size.unwrap_or(inferred); + if alphabet_size < inferred { + anyhow::bail!( + "--alphabet-size {} is smaller than max symbol + 1 ({}) in the strings", + alphabet_size, + inferred + ); + } + ( + ser(StringToStringCorrection::new( + alphabet_size, + source, + target, + bound_k, + ))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 39501490c..bbaf16165 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -34,6 +34,7 @@ {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, {"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]}, + {"problem":"StringToStringCorrection","variant":{},"instance":{"alphabet_size":4,"bound_k":2,"source":[0,1,2,3,1,0],"target":[0,1,3,2,1]},"samples":[{"config":[8,5],"metric":true}],"optimal":[{"config":[5,7],"metric":true},{"config":[8,5],"metric":true}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} ], diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cc96aa83e..99a7bc9c9 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -9,6 +9,7 @@ //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length +//! - [`StringToStringCorrection`]: String-to-String Correction (derive target via deletions and swaps) //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; @@ -19,6 +20,7 @@ mod longest_common_subsequence; mod minimum_tardiness_sequencing; pub(crate) mod paintshop; pub(crate) mod shortest_common_supersequence; +pub(crate) mod string_to_string_correction; mod subset_sum; pub use bin_packing::BinPacking; @@ -29,6 +31,7 @@ pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; pub use shortest_common_supersequence::ShortestCommonSupersequence; +pub use string_to_string_correction::StringToStringCorrection; pub use subset_sum::SubsetSum; #[cfg(feature = "example-db")] @@ -37,6 +40,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Source string (symbol indices)" }, + FieldInfo { name: "target", type_name: "Vec", description: "Target string (symbol indices)" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of operations allowed" }, + ], + } +} + +/// The String-to-String Correction problem. +/// +/// Given an alphabet of size `a`, a source string `s` over `{0, ..., a-1}`, +/// a target string `t` over the same alphabet, and a bound `K`, determine +/// whether `t` can be obtained from `s` by applying at most `K` operations, +/// where each operation is either a character deletion or a swap of two +/// adjacent characters. +/// +/// # Representation +/// +/// The configuration is a vector of length `K`. For a source string of +/// length `n`, each entry is in `{0, ..., 2n}`: +/// - Values `0..current_len` delete the character at that index in the +/// current working string. +/// - Values `current_len..2n` swap the character at position +/// `value - current_len` with its right neighbor. +/// - Value `2n` is a no-op (skip this slot). +/// +/// The domain size per slot is fixed at `2n + 1` regardless of how +/// deletions shorten the working string; as the working string shrinks, +/// some encodings that were valid before may become invalid. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::StringToStringCorrection; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // source = [0,1,2,3,1,0], target = [0,1,3,2,1], bound = 2 +/// let problem = StringToStringCorrection::new(4, vec![0,1,2,3,1,0], vec![0,1,3,2,1], 2); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringToStringCorrection { + alphabet_size: usize, + source: Vec, + target: Vec, + bound_k: usize, +} + +impl StringToStringCorrection { + /// Create a new StringToStringCorrection instance. + /// + /// # Panics + /// + /// Panics if `alphabet_size` is 0 when the source or target string is + /// non-empty, or if any symbol in `source` or `target` is + /// `>= alphabet_size`. + pub fn new( + alphabet_size: usize, + source: Vec, + target: Vec, + bound_k: usize, + ) -> Self { + assert!( + alphabet_size > 0 || (source.is_empty() && target.is_empty()), + "alphabet_size must be > 0 when source or target is non-empty" + ); + assert!( + source.iter().all(|&s| s < alphabet_size), + "all source symbols must be < alphabet_size" + ); + assert!( + target.iter().all(|&s| s < alphabet_size), + "all target symbols must be < alphabet_size" + ); + Self { + alphabet_size, + source, + target, + bound_k, + } + } + + /// Returns the alphabet size. + pub fn alphabet_size(&self) -> usize { + self.alphabet_size + } + + /// Returns the source string. + pub fn source(&self) -> &[usize] { + &self.source + } + + /// Returns the target string. + pub fn target(&self) -> &[usize] { + &self.target + } + + /// Returns the operation bound. + pub fn bound_k(&self) -> usize { + self.bound_k + } + + /// Returns the length of the source string. + pub fn source_length(&self) -> usize { + self.source.len() + } + + /// Returns the length of the target string. + pub fn target_length(&self) -> usize { + self.target.len() + } +} + +impl Problem for StringToStringCorrection { + const NAME: &'static str = "StringToStringCorrection"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2 * self.source.len() + 1; self.bound_k] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.bound_k { + return false; + } + let n = self.source.len(); + let domain = 2 * n + 1; + if config.iter().any(|&v| v >= domain) { + return false; + } + let noop = 2 * n; + let mut working = self.source.clone(); + for &op in config { + if op == noop { + // no-op + continue; + } + let current_len = working.len(); + if op < current_len { + // delete at index op + working.remove(op); + } else { + let swap_pos = op - current_len; + if swap_pos + 1 < current_len { + working.swap(swap_pos, swap_pos + 1); + } else { + // invalid operation for current string state + return false; + } + } + } + working == self.target + } +} + +impl SatisfactionProblem for StringToStringCorrection {} + +crate::declare_variants! { + default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound_k", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "string_to_string_correction", + build: || { + let problem = + StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + // source has length 6. Domain = 2*6+1 = 13. No-op = 12. + // First operation: swap at positions 2,3 in original 6-element string. + // current_len = 6, so swap range starts at 6. + // swap_pos = value - current_len. For swap_pos=2, value = 6 + 2 = 8 + // After swap: [0,1,3,2,1,0] + // Second operation: delete at position 5 (the trailing 0). + // current_len = 6, 5 < 6 → delete index 5 + // After delete: [0,1,3,2,1] = target + crate::example_db::specs::satisfaction_example(problem, vec![vec![8, 5]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/string_to_string_correction.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 94614519a..2ef2298ea 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -21,6 +21,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, ShortestCommonSupersequence, StringToStringCorrection, + SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/string_to_string_correction.rs b/src/unit_tests/models/misc/string_to_string_correction.rs new file mode 100644 index 000000000..50ef969c8 --- /dev/null +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -0,0 +1,133 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_string_to_string_correction_creation() { + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + assert_eq!(problem.alphabet_size(), 4); + assert_eq!(problem.source(), &[0, 1, 2, 3, 1, 0]); + assert_eq!(problem.target(), &[0, 1, 3, 2, 1]); + assert_eq!(problem.bound_k(), 2); + assert_eq!(problem.source_length(), 6); + assert_eq!(problem.target_length(), 5); + // domain = 2*6+1 = 13, bound_k = 2 + assert_eq!(problem.dims(), vec![13; 2]); + assert_eq!( + ::NAME, + "StringToStringCorrection" + ); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_string_to_string_correction_evaluation() { + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + // Known solution: swap positions 2&3 (value=8), then delete index 5 (value=5) + // Step 1: current_len=6, op=8 >= 6, swap_pos = 8-6=2, swap(2,3) → [0,1,3,2,1,0] + // Step 2: current_len=6, op=5 < 6, delete(5) → [0,1,3,2,1] = target + assert!(problem.evaluate(&[8, 5])); + // All no-ops should not produce target (source != target) + assert!(!problem.evaluate(&[12, 12])); +} + +#[test] +fn test_string_to_string_correction_invalid_operations() { + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + // out-of-domain values + assert!(!problem.evaluate(&[13, 5])); + assert!(!problem.evaluate(&[8, 13])); + // wrong length config + assert!(!problem.evaluate(&[8])); + assert!(!problem.evaluate(&[8, 5, 12])); +} + +#[test] +fn test_string_to_string_correction_invalid_after_deletion() { + // After a deletion, some swap indices become invalid + let problem = StringToStringCorrection::new(2, vec![0, 1, 0], vec![1], 2); + // source len = 3, domain = 7, noop = 6 + // op=0: delete index 0 → [1, 0], current_len=2 + // op=5: 5 >= 2, swap_pos = 5-2=3, need 3+1<2 → false → invalid + assert!(!problem.evaluate(&[0, 5])); +} + +#[test] +fn test_string_to_string_correction_serialization() { + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + let json = serde_json::to_value(&problem).unwrap(); + let restored: StringToStringCorrection = serde_json::from_value(json).unwrap(); + assert_eq!(restored.alphabet_size(), problem.alphabet_size()); + assert_eq!(restored.source(), problem.source()); + assert_eq!(restored.target(), problem.target()); + assert_eq!(restored.bound_k(), problem.bound_k()); +} + +#[test] +fn test_string_to_string_correction_solver() { + // Small instance: source [0,1], target [1,0], bound 1 + // Need a single swap: swap_pos=0, value = current_len + 0 = 2 + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 1); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_string_to_string_correction_paper_example() { + // Paper example: source [0,1,2,3,1,0], target [0,1,3,2,1], bound 2 + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + // Verify the known solution + assert!(problem.evaluate(&[8, 5])); + + // Verify all solutions with brute force + let solver = BruteForce::new(); + let all_solutions = solver.find_all_satisfying(&problem); + assert!(!all_solutions.is_empty()); + // The known solution must be among them + assert!(all_solutions.contains(&vec![8, 5])); + for sol in &all_solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_string_to_string_correction_unsatisfiable() { + // bound_k=0, source != target → impossible + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(!problem.evaluate(&[])); + + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_string_to_string_correction_identity() { + // source == target, bound_k=0 → satisfied with empty config + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![0, 1], 0); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_string_to_string_correction_empty_strings() { + // Both empty, bound_k=0 → trivially satisfied + let problem = StringToStringCorrection::new(0, vec![], vec![], 0); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_string_to_string_correction_delete_only() { + // source [0,1,2], target [0,2], bound 1 + // Delete index 1: op=1, current_len=3, 1<3 → delete → [0,2] = target + let problem = StringToStringCorrection::new(3, vec![0, 1, 2], vec![0, 2], 1); + assert!(problem.evaluate(&[1])); + + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index ef0f09f67..745fc06f8 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -170,6 +170,10 @@ fn test_all_problems_implement_trait_correctly() { &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3), "ShortestCommonSupersequence", ); + check_problem_trait( + &StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 1), + "StringToStringCorrection", + ); check_problem_trait( &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), "FlowShopScheduling", From f7d629e126bb95356392898162e1fdb6ca14993a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 23:36:01 +0800 Subject: [PATCH 3/7] chore: remove plan file after implementation --- .../2026-03-16-string-to-string-correction.md | 154 ------------------ 1 file changed, 154 deletions(-) delete mode 100644 docs/plans/2026-03-16-string-to-string-correction.md diff --git a/docs/plans/2026-03-16-string-to-string-correction.md b/docs/plans/2026-03-16-string-to-string-correction.md deleted file mode 100644 index 5cd91ddd9..000000000 --- a/docs/plans/2026-03-16-string-to-string-correction.md +++ /dev/null @@ -1,154 +0,0 @@ -# Plan: Add StringToStringCorrection Model - -**Issue:** #439 [Model] StringToStringCorrection -**Skill:** add-model -**Date:** 2026-03-16 - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `StringToStringCorrection` | -| 2 | Mathematical definition | Given finite alphabet Sigma, two strings x,y in Sigma*, and positive integer K. Can y be derived from x by K or fewer operations of single symbol deletion or adjacent symbol interchange? | -| 3 | Problem type | Satisfaction (bool) | -| 4 | Type parameters | None | -| 5 | Struct fields | `alphabet_size: usize`, `source: Vec`, `target: Vec`, `bound_k: usize` | -| 6 | Configuration space | `vec![2 * source.len() + 1; bound_k]` — K operation slots, each in {0..2*source_length} where 0..source_length = delete at position i, source_length..2*source_length = swap positions i and i+1, 2*source_length = no-op | -| 7 | Feasibility check | Apply operations left-to-right to mutable copy of source; skip no-ops; return false if any delete/swap index is out of bounds for current intermediate string | -| 8 | Objective function | `bool` — true iff result equals target after all operations | -| 9 | Best known algorithm | Brute-force: O((2*source_length+1)^bound_k) — Wagner (1975) proved NP-completeness | -| 10 | Solving strategy | BruteForce only | -| 11 | Category | `misc/` | -| 12 | Expected outcome | source=[0,1,2,3,1,0], target=[0,1,3,2,1], bound_k=2, alphabet_size=4. Solution: swap(2,3) then delete(5) → answer YES. Minimum cost is exactly 2. | - -**Associated rules:** #453 [Rule] SET COVERING to STRING-TO-STRING CORRECTION - -## Batch 1: Implementation (Steps 1-5.5) - -### Step 1: Create model file - -Create `src/models/misc/string_to_string_correction.rs` following `ShortestCommonSupersequence` as reference. - -**Schema entry:** -```rust -inventory::submit! { - ProblemSchemaEntry { - name: "StringToStringCorrection", - display_name: "String-to-String Correction", - aliases: &[], - dimensions: &[], - module_path: module_path!(), - description: "Derive target string from source using at most K deletions and adjacent swaps", - fields: &[ - FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the finite alphabet" }, - FieldInfo { name: "source", type_name: "Vec", description: "Source string (symbol indices)" }, - FieldInfo { name: "target", type_name: "Vec", description: "Target string (symbol indices)" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of operations allowed" }, - ], - } -} -``` - -**Struct:** -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StringToStringCorrection { - alphabet_size: usize, - source: Vec, - target: Vec, - bound_k: usize, -} -``` - -**Constructor:** Validate that `alphabet_size > 0` when source or target is non-empty, and all symbols in source/target are < alphabet_size. - -**Getters:** `alphabet_size()`, `source()`, `target()`, `bound_k()`, `source_length()`, `target_length()`. - -**Problem trait:** -- `NAME = "StringToStringCorrection"` -- `type Metric = bool` -- `variant() -> crate::variant_params![]` -- `dims() -> vec![2 * self.source.len() + 1; self.bound_k]` -- `evaluate()`: Apply operations left-to-right to a mutable Vec copy of source. For each operation slot: - - value < current_len → delete at that position - - value >= current_len && value < current_len + (current_len - 1) → swap adjacent at position (value - current_len) and (value - current_len + 1) - - value == 2 * source.len() (the no-op sentinel) - - Any other value → return false (invalid operation for current string state) - - After all ops, return result == target - - **IMPORTANT encoding note:** The config space is fixed at `2 * source.len() + 1` per slot. But as the string gets shorter from deletions, valid operation indices change. Values that were valid initially may become out-of-bounds after deletions. The evaluate function must handle this dynamically: check each operation against the *current* intermediate string length, not the original source length. - -**SatisfactionProblem impl:** marker trait, no methods. - -**declare_variants!:** -```rust -crate::declare_variants! { - default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound_k", -} -``` - -### Step 2: Register the model - -1. `src/models/misc/mod.rs` — add `pub(crate) mod string_to_string_correction;` and `pub use string_to_string_correction::StringToStringCorrection;` -2. `src/models/mod.rs` — add `StringToStringCorrection` to the `misc` re-export line -3. Update module doc comment in `misc/mod.rs` - -### Step 3: Register for CLI discovery - -In `problemreductions-cli/src/problem_name.rs` — no short alias needed (no well-established abbreviation). - -### Step 4: Add CLI creation support - -In `problemreductions-cli/src/commands/create.rs`: -- Add a match arm for `"StringToStringCorrection"` that parses `--source-string`, `--target-string`, `--bound`, `--alphabet-size` -- Source and target strings are comma-separated symbol indices (e.g., "0,1,2,3,1,0") - -In `problemreductions-cli/src/cli.rs`: -- Add `--source-string` and `--target-string` flags to `CreateArgs` -- Add entry to "Flags by problem type" help table -- Update `all_data_flags_empty()` - -Add example hint in `example_hint()`. - -### Step 5: Add canonical model example - -In the model file, add `canonical_model_example_specs()` function (feature-gated with `example-db`): -- Use the issue's example: alphabet_size=4, source=[0,1,2,3,1,0], target=[0,1,3,2,1], bound_k=2 -- Sample config: the known solution (swap at position 2, then delete at position 5) - -Register in `src/models/misc/mod.rs` `canonical_model_example_specs()`. - -### Step 6: Write unit tests - -Create `src/unit_tests/models/misc/string_to_string_correction.rs`: - -- `test_string_to_string_correction_creation` — construct instance, verify dims -- `test_string_to_string_correction_evaluation` — verify evaluate() on the known solution and invalid configs -- `test_string_to_string_correction_serialization` — round-trip serde -- `test_string_to_string_correction_solver` — BruteForce finds satisfying solution -- `test_string_to_string_correction_paper_example` — verify paper example instance, evaluate expected solution, check all satisfying solutions count -- `test_string_to_string_correction_invalid_operations` — test out-of-bounds operations return false -- `test_string_to_string_correction_unsatisfiable` — test case where no solution exists within bound - -Link test file via `#[cfg(test)] #[path = "..."] mod tests;`. - -### Step 7: Add trait_consistency entry - -In `src/unit_tests/trait_consistency.rs`: -- Add `check_problem_trait(...)` call with a small StringToStringCorrection instance -- No `test_direction` entry needed (satisfaction problem) - -## Batch 2: Paper Documentation (Step 6 from add-model) - -### Step 8: Write paper entry - -In `docs/paper/reductions.typ`: - -1. Add display name: `"StringToStringCorrection": [String-to-String Correction]` - -2. Write `problem-def("StringToStringCorrection")`: - - **Definition:** Given finite alphabet $Sigma$, source string $x in Sigma^*$, target string $y in Sigma^*$, and positive integer $K$, determine whether $y$ can be derived from $x$ by a sequence of at most $K$ operations, where each operation is either a single-symbol deletion or an adjacent-symbol interchange. - - **Background:** Classical NP-complete problem from Garey & Johnson (A4 SR20). Wagner (1975) proved NP-completeness via transformation from Set Covering. The standard edit distance (insert, delete, change) is solvable in O(|x|*|y|) time by Wagner-Fischer (1974), but restricting to only deletions and adjacent swaps makes the problem NP-complete. - - **Example:** Use the issue example with a visualization showing the source string, the two operations (swap + delete), and the resulting target string. - -3. Run `make paper` to verify compilation. From e817bc832f3d84efeb4ea7764d7c1e6e7f84fc78 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 03:07:12 +0800 Subject: [PATCH 4/7] fix: address PR #675 review feedback - reject negative StringToStringCorrection bounds in CLI creation - tighten model pruning and add integration coverage for help/create/solve flows - align StringToStringCorrection help and docs with brute-force solver usage --- docs/src/cli.md | 7 +- problemreductions-cli/src/cli.rs | 11 +- problemreductions-cli/src/commands/create.rs | 43 ++++++-- problemreductions-cli/tests/cli_tests.rs | 104 ++++++++++++++++++ src/lib.rs | 2 +- .../misc/string_to_string_correction.rs | 5 + src/models/mod.rs | 5 +- .../misc/string_to_string_correction.rs | 18 +++ 8 files changed, 176 insertions(+), 19 deletions(-) diff --git a/docs/src/cli.md b/docs/src/cli.md index 5c595e905..8cbfa0275 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -87,7 +87,8 @@ pred reduce problem.json --to QUBO -o reduced.json pred solve reduced.json --solver brute-force # Pipe commands together (use - to read from stdin) -pred create MIS --graph 0-1,1-2,2-3 | pred solve - +pred create MIS --graph 0-1,1-2,2-3 | pred solve - # when an ILP reduction path exists +pred create StringToStringCorrection --source-string "0,1,2,3,1,0" --target-string "0,1,3,2,1" --bound 2 | pred solve - --solver brute-force pred create MIS --graph 0-1,1-2,2-3 | pred reduce - --to QUBO | pred solve - ``` @@ -298,6 +299,7 @@ pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json +pred create StringToStringCorrection --source-string "0,1,2,3,1,0" --target-string "0,1,3,2,1" --bound 2 | pred solve - --solver brute-force ``` For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field @@ -320,7 +322,8 @@ pred create MaxCut --random --num-vertices 20 --edge-prob 0.5 -o maxcut.json Without `-o`, the problem JSON is printed to stdout, which can be piped to other commands: ```bash -pred create MIS --graph 0-1,1-2,2-3 | pred solve - +pred create MIS --graph 0-1,1-2,2-3 | pred solve - # when an ILP reduction path exists +pred create StringToStringCorrection --source-string "0,1,2,3,1,0" --target-string "0,1,3,2,1" --bound 2 | pred solve - --solver brute-force pred create MIS --random --num-vertices 10 | pred inspect - ``` diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index d9cd7a8f3..a09eb11e4 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -13,7 +13,8 @@ Typical workflow: pred evaluate problem.json --config 1,0,1,0 Piping (use - to read from stdin): - pred create MIS --graph 0-1,1-2 | pred solve - + pred create MIS --graph 0-1,1-2 | pred solve - # when an ILP reduction path exists + pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred create MIS --graph 0-1,1-2 | pred evaluate - --config 1,0,1 pred create MIS --graph 0-1,1-2 | pred reduce - --to QUBO @@ -269,6 +270,7 @@ Examples: pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" pred create QUBO --matrix \"1,0.5;0.5,2\" pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 + pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5 pred create MIS --random --num-vertices 10 --edge-prob 0.3 @@ -427,7 +429,7 @@ pub struct CreateArgs { /// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4") #[arg(long)] pub required_edges: Option, - /// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, or SCS) + /// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, SCS, or StringToStringCorrection) #[arg(long, allow_hyphen_values = true)] pub bound: Option, /// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0) @@ -472,7 +474,8 @@ Examples: pred solve problem.json --solver brute-force # brute-force (exhaustive search) pred solve reduced.json # solve a reduction bundle pred solve reduced.json -o solution.json # save result to file - pred create MIS --graph 0-1,1-2 | pred solve - # read from stdin + pred create MIS --graph 0-1,1-2 | pred solve - # read from stdin when an ILP path exists + pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred solve problem.json --timeout 10 # abort after 10 seconds Typical workflow: @@ -486,6 +489,8 @@ Solve via explicit reduction: Input: a problem JSON from `pred create`, or a reduction bundle from `pred reduce`. When given a bundle, the target is solved and the solution is mapped back to the source. The ILP solver auto-reduces non-ILP problems before solving. +Problems without an ILP reduction path, such as `LengthBoundedDisjointPaths` and +`StringToStringCorrection`, currently need `--solver brute-force`. ILP backend (default: HiGHS). To use a different backend: cargo install problemreductions-cli --features coin-cbc diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5a7d44064..56f9b3339 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -373,12 +373,7 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); } else { let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type); - eprintln!( - " --{:<16} {} ({})", - help_flag_name(canonical, &field.name), - field.description, - hint - ); + eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); } } } else { @@ -415,7 +410,15 @@ fn problem_help_flag_name( if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" { return "bound".to_string(); } - field_name.replace('_', "-") + if canonical == "StringToStringCorrection" { + return match field_name { + "source" => "source-string".to_string(), + "target" => "target-string".to_string(), + "bound_k" => "bound".to_string(), + _ => help_flag_name(canonical, field_name), + }; + } + help_flag_name(canonical, field_name) } fn lbdp_validation_error(message: &str, usage: Option<&str>) -> anyhow::Error { @@ -1557,9 +1560,13 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let target_str = args.target_string.as_deref().ok_or_else(|| { anyhow::anyhow!("StringToStringCorrection requires --target-string\n\n{usage}") })?; - let bound_k = args.bound.ok_or_else(|| { - anyhow::anyhow!("StringToStringCorrection requires --bound\n\n{usage}") - })? as usize; + let bound_k = parse_nonnegative_usize_bound( + args.bound.ok_or_else(|| { + anyhow::anyhow!("StringToStringCorrection requires --bound\n\n{usage}") + })?, + "StringToStringCorrection", + usage, + )?; let parse_symbols = |s: &str| -> Result> { if s.trim().is_empty() { return Ok(Vec::new()); @@ -2487,4 +2494,20 @@ mod tests { "num-paths-required" ); } + + #[test] + fn test_problem_help_uses_string_to_string_correction_cli_flags() { + assert_eq!( + problem_help_flag_name("StringToStringCorrection", "source", "Vec", false), + "source-string" + ); + assert_eq!( + problem_help_flag_name("StringToStringCorrection", "target", "Vec", false), + "target-string" + ); + assert_eq!( + problem_help_flag_name("StringToStringCorrection", "bound_k", "usize", false), + "bound" + ); + } } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..563379a35 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -33,6 +33,15 @@ fn test_list_includes_undirected_two_commodity_integral_flow() { assert!(stdout.contains("UndirectedTwoCommodityIntegralFlow")); } +#[test] +fn test_solve_help_mentions_string_to_string_correction_bruteforce() { + let output = pred().args(["solve", "--help"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("StringToStringCorrection"), "stdout: {stdout}"); + assert!(stdout.contains("--solver brute-force"), "stdout: {stdout}"); +} + #[test] fn test_list_rules() { let output = pred().args(["list", "--rules"]).output().unwrap(); @@ -1961,6 +1970,101 @@ fn test_create_scs_rejects_negative_bound() { assert!(stderr.contains("nonnegative --bound"), "stderr: {stderr}"); } +#[test] +fn test_create_string_to_string_correction() { + let output_file = + std::env::temp_dir().join("pred_test_create_string_to_string_correction.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "StringToStringCorrection", + "--source-string", + "0,1,2,3,1,0", + "--target-string", + "0,1,3,2,1", + "--bound", + "2", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "StringToStringCorrection"); + assert_eq!( + json["data"]["source"], + serde_json::json!([0, 1, 2, 3, 1, 0]) + ); + assert_eq!(json["data"]["target"], serde_json::json!([0, 1, 3, 2, 1])); + assert_eq!(json["data"]["bound_k"], 2); + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_model_example_string_to_string_correction() { + let output = pred() + .args(["create", "--example", "StringToStringCorrection"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "StringToStringCorrection"); + assert_eq!( + json["data"]["source"], + serde_json::json!([0, 1, 2, 3, 1, 0]) + ); + assert_eq!(json["data"]["target"], serde_json::json!([0, 1, 3, 2, 1])); + assert_eq!(json["data"]["bound_k"], 2); +} + +#[test] +fn test_create_string_to_string_correction_help_uses_cli_flags() { + let output = pred() + .args(["create", "StringToStringCorrection"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--source-string"), "stderr: {stderr}"); + assert!(stderr.contains("--target-string"), "stderr: {stderr}"); + assert!(stderr.contains("--bound"), "stderr: {stderr}"); + assert!(!stderr.contains("--bound-k"), "stderr: {stderr}"); +} + +#[test] +fn test_create_string_to_string_correction_rejects_negative_bound() { + let output = pred() + .args([ + "create", + "StringToStringCorrection", + "--source-string", + "0,1,2,3,1,0", + "--target-string", + "0,1,3,2,1", + "--bound", + "-1", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "negative bound should be rejected" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("nonnegative --bound"), "stderr: {stderr}"); +} + #[test] fn test_create_spinglass() { let output_file = std::env::temp_dir().join("pred_test_create_sg.json"); diff --git a/src/lib.rs b/src/lib.rs index 42bc6c927..67afad296 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ pub mod prelude { pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index aee8bdc9c..bed18f670 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -158,6 +158,11 @@ impl Problem for StringToStringCorrection { if config.len() != self.bound_k { return false; } + if self.target.len() > self.source.len() + || self.target.len() < self.source.len().saturating_sub(self.bound_k) + { + return false; + } let n = self.source.len(); let domain = 2 * n + 1; if config.iter().any(|&v| v >= domain) { diff --git a/src/models/mod.rs b/src/models/mod.rs index 83f080584..27fc6b871 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -22,8 +22,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, StringToStringCorrection, - SubsetSum, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, + StringToStringCorrection, SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/string_to_string_correction.rs b/src/unit_tests/models/misc/string_to_string_correction.rs index 50ef969c8..f30b84bc3 100644 --- a/src/unit_tests/models/misc/string_to_string_correction.rs +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -131,3 +131,21 @@ fn test_string_to_string_correction_delete_only() { .expect("should find a solution"); assert!(problem.evaluate(&solution)); } + +#[test] +fn test_string_to_string_correction_rejects_target_longer_than_source() { + let problem = StringToStringCorrection::new(3, vec![0, 1], vec![0, 1, 2], 1); + assert!(!problem.evaluate(&[4])); +} + +#[test] +fn test_string_to_string_correction_rejects_excessive_deletions_requirement() { + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3], vec![0], 2); + assert!(!problem.evaluate(&[8, 8])); +} + +#[test] +fn test_string_to_string_correction_is_available_in_prelude() { + let problem = crate::prelude::StringToStringCorrection::new(2, vec![0], vec![0], 0); + assert!(problem.evaluate(&[])); +} From fe2c8a47d5d3c8eae3ec94032223ab192f7ade71 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 18 Mar 2026 15:51:34 +0800 Subject: [PATCH 5/7] Fix paper figure highlighting logic for repeated symbols The previous highlighting used value-based comparison via `.position()`, which finds the first occurrence and would produce incorrect highlights for strings with repeated symbols. Now compares by position index directly. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 0bd5f269b..58175fc19 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2304,7 +2304,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS // Step 1: after swap at positions 2,3 stack(dir: ltr, spacing: 0pt, box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(2,3)$: quad])), - ..after-swap.map(c => cell(alpha-map.at(c), highlight: c != source.at(after-swap.position(cc => cc == c)))), + ..range(after-swap.len()).map(i => cell(alpha-map.at(after-swap.at(i)), highlight: after-swap.at(i) != source.at(i))), ), // Step 2: after delete at position 5 stack(dir: ltr, spacing: 0pt, From c7442b6d93eb3bf78b1943fa55764d774764c1d0 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 18 Mar 2026 16:00:43 +0800 Subject: [PATCH 6/7] Rename bound_k to bound for consistency with other models ShortestCommonSupersequence and other models use `bound` as the field name. Rename for consistency across the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- problemreductions-cli/src/commands/create.rs | 8 ++++---- problemreductions-cli/tests/cli_tests.rs | 4 ++-- src/example_db/fixtures/examples.json | 4 ++-- .../misc/string_to_string_correction.rs | 20 +++++++++---------- .../misc/string_to_string_correction.rs | 8 ++++---- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 58175fc19..c4f0cd36b 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2266,7 +2266,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS let source = x.instance.source let target = x.instance.target let alpha-size = x.instance.alphabet_size - let bound-k = x.instance.bound_k + let bound-k = x.instance.bound let n = source.len() // Alphabet mapping: 0->a, 1->b, 2->c, 3->d let alpha-map = range(alpha-size).map(i => str.from-unicode(97 + i)) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index fa5ccf934..5a0faeca4 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -462,7 +462,7 @@ fn problem_help_flag_name( return match field_name { "source" => "source-string".to_string(), "target" => "target-string".to_string(), - "bound_k" => "bound".to_string(), + "bound" => "bound".to_string(), _ => help_flag_name(canonical, field_name), }; } @@ -1802,7 +1802,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let target_str = args.target_string.as_deref().ok_or_else(|| { anyhow::anyhow!("StringToStringCorrection requires --target-string\n\n{usage}") })?; - let bound_k = parse_nonnegative_usize_bound( + let bound = parse_nonnegative_usize_bound( args.bound.ok_or_else(|| { anyhow::anyhow!("StringToStringCorrection requires --bound\n\n{usage}") })?, @@ -1838,7 +1838,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { alphabet_size, source, target, - bound_k, + bound, ))?, resolved_variant.clone(), ) @@ -3063,7 +3063,7 @@ mod tests { "target-string" ); assert_eq!( - problem_help_flag_name("StringToStringCorrection", "bound_k", "usize", false), + problem_help_flag_name("StringToStringCorrection", "bound", "usize", false), "bound" ); } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index f996a6b69..6d2f94d1d 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -2306,7 +2306,7 @@ fn test_create_string_to_string_correction() { serde_json::json!([0, 1, 2, 3, 1, 0]) ); assert_eq!(json["data"]["target"], serde_json::json!([0, 1, 3, 2, 1])); - assert_eq!(json["data"]["bound_k"], 2); + assert_eq!(json["data"]["bound"], 2); std::fs::remove_file(&output_file).ok(); } @@ -2329,7 +2329,7 @@ fn test_create_model_example_string_to_string_correction() { serde_json::json!([0, 1, 2, 3, 1, 0]) ); assert_eq!(json["data"]["target"], serde_json::json!([0, 1, 3, 2, 1])); - assert_eq!(json["data"]["bound_k"], 2); + assert_eq!(json["data"]["bound"], 2); } #[test] diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 1103050f3..10b81634e 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -43,7 +43,7 @@ {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, {"problem":"StaffScheduling","variant":{},"instance":{"num_workers":4,"requirements":[2,2,2,3,3,2,1],"schedules":[[true,true,true,true,true,false,false],[false,true,true,true,true,true,false],[false,false,true,true,true,true,true],[true,false,false,true,true,true,true],[true,true,false,false,true,true,true]],"shifts_per_schedule":5},"samples":[{"config":[1,1,1,1,0],"metric":true}],"optimal":[{"config":[0,1,1,1,1],"metric":true},{"config":[0,2,0,1,1],"metric":true},{"config":[0,2,0,2,0],"metric":true},{"config":[1,0,1,1,1],"metric":true},{"config":[1,0,2,0,1],"metric":true},{"config":[1,1,0,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true},{"config":[1,1,0,2,0],"metric":true},{"config":[1,1,1,0,1],"metric":true},{"config":[1,1,1,1,0],"metric":true},{"config":[1,2,0,0,1],"metric":true},{"config":[1,2,0,1,0],"metric":true},{"config":[2,0,0,1,1],"metric":true},{"config":[2,0,0,2,0],"metric":true},{"config":[2,0,1,0,1],"metric":true},{"config":[2,0,1,1,0],"metric":true},{"config":[2,0,2,0,0],"metric":true},{"config":[2,1,0,0,1],"metric":true},{"config":[2,1,0,1,0],"metric":true},{"config":[2,1,1,0,0],"metric":true}]}, {"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]}, - {"problem":"StringToStringCorrection","variant":{},"instance":{"alphabet_size":4,"bound_k":2,"source":[0,1,2,3,1,0],"target":[0,1,3,2,1]},"samples":[{"config":[8,5],"metric":true}],"optimal":[{"config":[5,7],"metric":true},{"config":[8,5],"metric":true}]}, + {"problem":"StringToStringCorrection","variant":{},"instance":{"alphabet_size":4,"bound":2,"source":[0,1,2,3,1,0],"target":[0,1,3,2,1]},"samples":[{"config":[8,5],"metric":true}],"optimal":[{"config":[5,7],"metric":true},{"config":[8,5],"metric":true}]}, {"problem":"StrongConnectivityAugmentation","variant":{"weight":"i32"},"instance":{"bound":1,"candidate_arcs":[[3,0,5],[3,1,3],[3,2,4],[4,0,6],[4,1,2],[4,2,7],[5,0,4],[5,1,3],[5,2,1],[0,3,8],[0,4,3],[0,5,2],[1,3,6],[1,4,4],[1,5,5],[2,4,3],[2,5,7],[1,0,2]],"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[3,4,null],[4,3,null],[2,3,null],[4,5,null],[5,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true},{"config":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} @@ -60,7 +60,7 @@ {"source":{"problem":"KSatisfiability","variant":{"k":"K2"},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,4]},{"literals":[-3,-4]}],"num_vars":4}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[0.0,1.0,-1.0,0.0],[0.0,0.0,0.0,-1.0],[0.0,0.0,0.0,1.0],[0.0,0.0,0.0,0.0]],"num_vars":4}},"solutions":[{"source_config":[0,1,0,1],"target_config":[0,1,0,1]}]}, {"source":{"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,-3]},{"literals":[-1,3,4]},{"literals":[2,-4,5]},{"literals":[-2,3,-5]},{"literals":[1,-3,5]},{"literals":[-1,-2,4]},{"literals":[3,-4,-5]}],"num_vars":5}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[0.0,4.0,-4.0,0.0,0.0,4.0,-4.0,0.0,0.0,4.0,-4.0,0.0],[0.0,0.0,-2.0,-2.0,0.0,4.0,0.0,4.0,-4.0,0.0,-4.0,0.0],[0.0,0.0,2.0,-2.0,0.0,1.0,4.0,0.0,4.0,-4.0,0.0,4.0],[0.0,0.0,0.0,4.0,0.0,0.0,-1.0,-4.0,0.0,0.0,-1.0,-4.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,1.0,-1.0,0.0,1.0],[0.0,0.0,0.0,0.0,0.0,-2.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,2.0]],"num_vars":12}},"solutions":[{"source_config":[1,1,1,1,1],"target_config":[1,1,1,1,1,0,0,0,0,0,1,0]}]}, {"source":{"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,3]},{"literals":[-1,-2,3]}],"num_vars":3}},"target":{"problem":"SubsetSum","variant":{},"instance":{"sizes":["10010","10001","1010","1001","111","100","10","20","1","2"],"target":"11144"}},"solutions":[{"source_config":[0,0,1],"target_config":[0,1,0,1,1,0,1,1,1,0]}]}, - {"source":{"problem":"KSatisfiability","variant":{"k":"KN"},"instance":{"clauses":[{"literals":[1,-2,3]},{"literals":[-1,3,4]},{"literals":[2,-3,-4]}],"num_vars":4}},"target":{"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,-2,3]},{"literals":[-1,3,4]},{"literals":[2,-3,-4]}],"num_vars":4}},"solutions":[{"source_config":[1,1,1,0],"target_config":[1,1,1,0]}]}, + {"source":{"problem":"KSatisfiability","variant":{"k":"KN"},"instance":{"clauses":[{"literals":[1,-2,3]},{"literals":[-1,3,4]},{"literals":[2,-3,-4]}],"num_vars":4}},"target":{"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,-2,3]},{"literals":[-1,3,4]},{"literals":[2,-3,-4]}],"num_vars":4}},"solutions":[{"source_config":[1,1,1,1],"target_config":[1,1,1,1]}]}, {"source":{"problem":"Knapsack","variant":{},"instance":{"capacity":7,"values":[3,4,5,7],"weights":[2,3,4,5]}},"target":{"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[-483.0,240.0,320.0,400.0,80.0,160.0,320.0],[0.0,-664.0,480.0,600.0,120.0,240.0,480.0],[0.0,0.0,-805.0,800.0,160.0,320.0,640.0],[0.0,0.0,0.0,-907.0,200.0,400.0,800.0],[0.0,0.0,0.0,0.0,-260.0,80.0,160.0],[0.0,0.0,0.0,0.0,0.0,-480.0,320.0],[0.0,0.0,0.0,0.0,0.0,0.0,-800.0]],"num_vars":7}},"solutions":[{"source_config":[1,0,0,1],"target_config":[1,0,0,1,0,0,0]}]}, {"source":{"problem":"LongestCommonSubsequence","variant":{},"instance":{"strings":[[65,66,65,67],[66,65,67,65]]}},"target":{"problem":"ILP","variant":{"variable":"bool"},"instance":{"constraints":[{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[1,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[3,1.0],[4,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[3,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[4,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[0,1.0],[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[2,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[3,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[1,1.0],[5,1.0]]},{"cmp":"Le","rhs":1.0,"terms":[[4,1.0],[5,1.0]]}],"num_vars":6,"objective":[[0,1.0],[1,1.0],[2,1.0],[3,1.0],[4,1.0],[5,1.0]],"sense":"Maximize"}},"solutions":[{"source_config":[0,1,1,1],"target_config":[0,0,1,1,0,1]}]}, {"source":{"problem":"MaxCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,4,null],[0,5,null],[1,2,null],[1,6,null],[2,3,null],[2,7,null],[3,4,null],[3,8,null],[4,9,null],[5,7,null],[5,8,null],[6,8,null],[6,9,null],[7,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}}}},"target":{"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"fields":[0,0,0,0,0,0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,4,null],[0,5,null],[1,2,null],[1,6,null],[2,3,null],[2,7,null],[3,4,null],[3,8,null],[4,9,null],[5,7,null],[5,8,null],[6,8,null],[6,9,null],[7,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}}}},"solutions":[{"source_config":[1,0,1,0,0,0,0,0,1,1],"target_config":[1,0,1,0,0,0,0,0,1,1]}]}, diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index bed18f670..d063c2bc1 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -30,7 +30,7 @@ inventory::submit! { FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the finite alphabet" }, FieldInfo { name: "source", type_name: "Vec", description: "Source string (symbol indices)" }, FieldInfo { name: "target", type_name: "Vec", description: "Target string (symbol indices)" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of operations allowed" }, + FieldInfo { name: "bound", type_name: "usize", description: "Maximum number of operations allowed" }, ], } } @@ -74,7 +74,7 @@ pub struct StringToStringCorrection { alphabet_size: usize, source: Vec, target: Vec, - bound_k: usize, + bound: usize, } impl StringToStringCorrection { @@ -89,7 +89,7 @@ impl StringToStringCorrection { alphabet_size: usize, source: Vec, target: Vec, - bound_k: usize, + bound: usize, ) -> Self { assert!( alphabet_size > 0 || (source.is_empty() && target.is_empty()), @@ -107,7 +107,7 @@ impl StringToStringCorrection { alphabet_size, source, target, - bound_k, + bound, } } @@ -127,8 +127,8 @@ impl StringToStringCorrection { } /// Returns the operation bound. - pub fn bound_k(&self) -> usize { - self.bound_k + pub fn bound(&self) -> usize { + self.bound } /// Returns the length of the source string. @@ -151,15 +151,15 @@ impl Problem for StringToStringCorrection { } fn dims(&self) -> Vec { - vec![2 * self.source.len() + 1; self.bound_k] + vec![2 * self.source.len() + 1; self.bound] } fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound_k { + if config.len() != self.bound { return false; } if self.target.len() > self.source.len() - || self.target.len() < self.source.len().saturating_sub(self.bound_k) + || self.target.len() < self.source.len().saturating_sub(self.bound) { return false; } @@ -196,7 +196,7 @@ impl Problem for StringToStringCorrection { impl SatisfactionProblem for StringToStringCorrection {} crate::declare_variants! { - default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound_k", + default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/misc/string_to_string_correction.rs b/src/unit_tests/models/misc/string_to_string_correction.rs index f30b84bc3..9b4c25063 100644 --- a/src/unit_tests/models/misc/string_to_string_correction.rs +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -8,10 +8,10 @@ fn test_string_to_string_correction_creation() { assert_eq!(problem.alphabet_size(), 4); assert_eq!(problem.source(), &[0, 1, 2, 3, 1, 0]); assert_eq!(problem.target(), &[0, 1, 3, 2, 1]); - assert_eq!(problem.bound_k(), 2); + assert_eq!(problem.bound(), 2); assert_eq!(problem.source_length(), 6); assert_eq!(problem.target_length(), 5); - // domain = 2*6+1 = 13, bound_k = 2 + // domain = 2*6+1 = 13, bound = 2 assert_eq!(problem.dims(), vec![13; 2]); assert_eq!( ::NAME, @@ -60,7 +60,7 @@ fn test_string_to_string_correction_serialization() { assert_eq!(restored.alphabet_size(), problem.alphabet_size()); assert_eq!(restored.source(), problem.source()); assert_eq!(restored.target(), problem.target()); - assert_eq!(restored.bound_k(), problem.bound_k()); + assert_eq!(restored.bound(), problem.bound()); } #[test] @@ -95,7 +95,7 @@ fn test_string_to_string_correction_paper_example() { #[test] fn test_string_to_string_correction_unsatisfiable() { - // bound_k=0, source != target → impossible + // bound=0, source != target → impossible let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 0); assert_eq!(problem.dims(), Vec::::new()); assert!(!problem.evaluate(&[])); From 0db6b71ed132f272cdaf79d8de9164020c976226 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 18 Mar 2026 16:06:22 +0800 Subject: [PATCH 7/7] Fix formatting issues in PR code Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 3 +-- problemreductions-cli/tests/cli_tests.rs | 5 ++++- src/models/misc/string_to_string_correction.rs | 7 +------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5a0faeca4..25e4454c5 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -15,8 +15,7 @@ use problemreductions::models::graph::{ use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, - StringToStringCorrection, - SubsetSum, + StringToStringCorrection, SubsetSum, }; use problemreductions::models::BiconnectivityAugmentation; use problemreductions::prelude::*; diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 6d2f94d1d..112c102c2 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -38,7 +38,10 @@ fn test_solve_help_mentions_string_to_string_correction_bruteforce() { let output = pred().args(["solve", "--help"]).output().unwrap(); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("StringToStringCorrection"), "stdout: {stdout}"); + assert!( + stdout.contains("StringToStringCorrection"), + "stdout: {stdout}" + ); assert!(stdout.contains("--solver brute-force"), "stdout: {stdout}"); } diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index d063c2bc1..a35af8c69 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -85,12 +85,7 @@ impl StringToStringCorrection { /// Panics if `alphabet_size` is 0 when the source or target string is /// non-empty, or if any symbol in `source` or `target` is /// `>= alphabet_size`. - pub fn new( - alphabet_size: usize, - source: Vec, - target: Vec, - bound: usize, - ) -> Self { + pub fn new(alphabet_size: usize, source: Vec, target: Vec, bound: usize) -> Self { assert!( alphabet_size > 0 || (source.is_empty() && target.is_empty()), "alphabet_size must be > 0 when source or target is non-empty"