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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"SequencingWithinIntervals": [Sequencing Within Intervals],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"StringToStringCorrection": [String-to-String Correction],
)

// Definition label: "def:<ProblemName>" — each definition block must have a matching label
Expand Down Expand Up @@ -2260,6 +2261,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
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(<wagner1975>, 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])),
..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,
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.],
) <fig:stsc>

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$.
][
Expand Down
20 changes: 20 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
@@ -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},
Expand Down
7 changes: 5 additions & 2 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
```

Expand Down Expand Up @@ -352,6 +353,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
pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" --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" --bound 1 -o sca.json
```

Expand All @@ -375,7 +377,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 -
```

Expand Down
20 changes: 16 additions & 4 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -257,6 +258,7 @@ Flags by problem type:
StaffScheduling --schedules, --requirements, --num-workers, --k
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)

Expand All @@ -275,6 +277,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
Expand Down Expand Up @@ -447,7 +450,7 @@ pub struct CreateArgs {
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
#[arg(long)]
pub required_edges: Option<String>,
/// 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<i64>,
/// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0)
Expand Down Expand Up @@ -492,9 +495,15 @@ pub struct CreateArgs {
/// Number of available workers for StaffScheduling
#[arg(long)]
pub num_workers: Option<u64>,
/// 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<usize>,
/// Source string for StringToStringCorrection (comma-separated symbol indices, e.g., "0,1,2,3")
#[arg(long)]
pub source_string: Option<String>,
/// Target string for StringToStringCorrection (comma-separated symbol indices, e.g., "0,1,3,2")
#[arg(long)]
pub target_string: Option<String>,
}

#[derive(clap::Args)]
Expand All @@ -504,7 +513,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:
Expand All @@ -518,6 +528,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
Expand Down
102 changes: 92 additions & 10 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use problemreductions::models::graph::{
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
MultiprocessorScheduling, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence,
SubsetSum,
StringToStringCorrection, SubsetSum,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -94,6 +94,15 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.requirements.is_none()
&& args.num_workers.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()
&& args.source_2.is_none()
&& args.sink_2.is_none()
&& args.requirement_1.is_none()
&& args.requirement_2.is_none()
}

fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> {
Expand Down Expand Up @@ -321,6 +330,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
}
"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"
}
_ => "",
}
}
Expand Down Expand Up @@ -408,12 +420,7 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
);
} 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 {
Expand Down Expand Up @@ -450,10 +457,15 @@ fn problem_help_flag_name(
if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" {
return "bound".to_string();
}
if canonical == "StaffScheduling" && field_name == "shifts_per_schedule" {
return "k".to_string();
if canonical == "StringToStringCorrection" {
return match field_name {
"source" => "source-string".to_string(),
"target" => "target-string".to_string(),
"bound" => "bound".to_string(),
_ => help_flag_name(canonical, field_name),
};
}
field_name.replace('_', "-")
help_flag_name(canonical, field_name)
}

fn lbdp_validation_error(message: &str, usage: Option<&str>) -> anyhow::Error {
Expand Down Expand Up @@ -1779,6 +1791,58 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
resolved_variant.clone(),
)
}

// 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 = 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<Vec<usize>> {
if s.trim().is_empty() {
return Ok(Vec::new());
}
s.split(',')
.map(|v| v.trim().parse::<usize>().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,
))?,
resolved_variant.clone(),
)
}

_ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)),
};

Expand Down Expand Up @@ -2987,6 +3051,22 @@ mod tests {
);
}

#[test]
fn test_problem_help_uses_string_to_string_correction_cli_flags() {
assert_eq!(
problem_help_flag_name("StringToStringCorrection", "source", "Vec<usize>", false),
"source-string"
);
assert_eq!(
problem_help_flag_name("StringToStringCorrection", "target", "Vec<usize>", false),
"target-string"
);
assert_eq!(
problem_help_flag_name("StringToStringCorrection", "bound", "usize", false),
"bound"
);
}

#[test]
fn test_problem_help_uses_k_for_staff_scheduling() {
assert_eq!(
Expand Down Expand Up @@ -3160,6 +3240,8 @@ mod tests {
deadline: None,
num_processors: None,
alphabet_size: None,
source_string: None,
target_string: None,
schedules: None,
requirements: None,
num_workers: None,
Expand Down
Loading
Loading