problemreductions/models/misc/string_to_string_correction.rs
1//! String-to-String Correction problem implementation.
2//!
3//! Given a source string `s` and a target string `t` over a finite alphabet,
4//! and a bound `K`, the problem asks whether `t` can be derived from `s`
5//! using at most `K` operations, where each operation is either a deletion
6//! of a character or a swap of two adjacent characters.
7//!
8//! The configuration is a vector of length `K`, where each entry encodes one
9//! operation. For a source of length `n`, each entry is in `{0, ..., 2n}`:
10//! - `0..current_len` → delete the character at that index
11//! - `current_len..2n` → swap the character at position `value - current_len`
12//! with its right neighbor
13//! - `2n` → no-op (skip this operation slot)
14//!
15//! This problem is NP-complete (Wagner, 1975).
16
17use crate::registry::{FieldInfo, ProblemSchemaEntry};
18use crate::traits::Problem;
19use serde::{Deserialize, Serialize};
20
21inventory::submit! {
22 ProblemSchemaEntry {
23 name: "StringToStringCorrection",
24 display_name: "String-to-String Correction",
25 aliases: &[],
26 dimensions: &[],
27 module_path: module_path!(),
28 description: "Derive target string from source using at most K deletions and adjacent swaps",
29 fields: &[
30 FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the finite alphabet" },
31 FieldInfo { name: "source", type_name: "Vec<usize>", description: "Source string (symbol indices)" },
32 FieldInfo { name: "target", type_name: "Vec<usize>", description: "Target string (symbol indices)" },
33 FieldInfo { name: "bound", type_name: "usize", description: "Maximum number of operations allowed" },
34 ],
35 }
36}
37
38/// The String-to-String Correction problem.
39///
40/// Given an alphabet of size `a`, a source string `s` over `{0, ..., a-1}`,
41/// a target string `t` over the same alphabet, and a bound `K`, determine
42/// whether `t` can be obtained from `s` by applying at most `K` operations,
43/// where each operation is either a character deletion or a swap of two
44/// adjacent characters.
45///
46/// # Representation
47///
48/// The configuration is a vector of length `K`. For a source string of
49/// length `n`, each entry is in `{0, ..., 2n}`:
50/// - Values `0..current_len` delete the character at that index in the
51/// current working string.
52/// - Values `current_len..2n` swap the character at position
53/// `value - current_len` with its right neighbor.
54/// - Value `2n` is a no-op (skip this slot).
55///
56/// The domain size per slot is fixed at `2n + 1` regardless of how
57/// deletions shorten the working string; as the working string shrinks,
58/// some encodings that were valid before may become invalid.
59///
60/// # Example
61///
62/// ```
63/// use problemreductions::models::misc::StringToStringCorrection;
64/// use problemreductions::{Problem, Solver, BruteForce};
65///
66/// // source = [0,1,2,3,1,0], target = [0,1,3,2,1], bound = 2
67/// let problem = StringToStringCorrection::new(4, vec![0,1,2,3,1,0], vec![0,1,3,2,1], 2);
68/// let solver = BruteForce::new();
69/// let solution = solver.find_witness(&problem);
70/// assert!(solution.is_some());
71/// ```
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct StringToStringCorrection {
74 alphabet_size: usize,
75 source: Vec<usize>,
76 target: Vec<usize>,
77 bound: usize,
78}
79
80impl StringToStringCorrection {
81 /// Create a new StringToStringCorrection instance.
82 ///
83 /// # Panics
84 ///
85 /// Panics if `alphabet_size` is 0 when the source or target string is
86 /// non-empty, or if any symbol in `source` or `target` is
87 /// `>= alphabet_size`.
88 pub fn new(alphabet_size: usize, source: Vec<usize>, target: Vec<usize>, bound: usize) -> Self {
89 assert!(
90 alphabet_size > 0 || (source.is_empty() && target.is_empty()),
91 "alphabet_size must be > 0 when source or target is non-empty"
92 );
93 assert!(
94 source.iter().all(|&s| s < alphabet_size),
95 "all source symbols must be < alphabet_size"
96 );
97 assert!(
98 target.iter().all(|&s| s < alphabet_size),
99 "all target symbols must be < alphabet_size"
100 );
101 Self {
102 alphabet_size,
103 source,
104 target,
105 bound,
106 }
107 }
108
109 /// Returns the alphabet size.
110 pub fn alphabet_size(&self) -> usize {
111 self.alphabet_size
112 }
113
114 /// Returns the source string.
115 pub fn source(&self) -> &[usize] {
116 &self.source
117 }
118
119 /// Returns the target string.
120 pub fn target(&self) -> &[usize] {
121 &self.target
122 }
123
124 /// Returns the operation bound.
125 pub fn bound(&self) -> usize {
126 self.bound
127 }
128
129 /// Returns the length of the source string.
130 pub fn source_length(&self) -> usize {
131 self.source.len()
132 }
133
134 /// Returns the length of the target string.
135 pub fn target_length(&self) -> usize {
136 self.target.len()
137 }
138}
139
140impl Problem for StringToStringCorrection {
141 const NAME: &'static str = "StringToStringCorrection";
142 type Value = crate::types::Or;
143
144 fn variant() -> Vec<(&'static str, &'static str)> {
145 crate::variant_params![]
146 }
147
148 fn dims(&self) -> Vec<usize> {
149 vec![2 * self.source.len() + 1; self.bound]
150 }
151
152 fn evaluate(&self, config: &[usize]) -> crate::types::Or {
153 crate::types::Or({
154 if config.len() != self.bound {
155 return crate::types::Or(false);
156 }
157 if self.target.len() > self.source.len()
158 || self.target.len() < self.source.len().saturating_sub(self.bound)
159 {
160 return crate::types::Or(false);
161 }
162 let n = self.source.len();
163 let domain = 2 * n + 1;
164 if config.iter().any(|&v| v >= domain) {
165 return crate::types::Or(false);
166 }
167 let noop = 2 * n;
168 let mut working = self.source.clone();
169 for &op in config {
170 if op == noop {
171 // no-op
172 continue;
173 }
174 let current_len = working.len();
175 if op < current_len {
176 // delete at index op
177 working.remove(op);
178 } else {
179 let swap_pos = op - current_len;
180 if swap_pos + 1 < current_len {
181 working.swap(swap_pos, swap_pos + 1);
182 } else {
183 // invalid operation for current string state
184 return crate::types::Or(false);
185 }
186 }
187 }
188 working == self.target
189 })
190 }
191}
192
193crate::declare_variants! {
194 default StringToStringCorrection => "(2 * source_length + 1) ^ bound",
195}
196
197#[cfg(feature = "example-db")]
198pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
199 vec![crate::example_db::specs::ModelExampleSpec {
200 id: "string_to_string_correction",
201 // source has length 6. Domain = 2*6+1 = 13. No-op = 12.
202 // First operation: swap at positions 2,3 → value = 6 + 2 = 8
203 // Second operation: delete at position 5
204 instance: Box::new(StringToStringCorrection::new(
205 4,
206 vec![0, 1, 2, 3, 1, 0],
207 vec![0, 1, 3, 2, 1],
208 2,
209 )),
210 optimal_config: vec![8, 5],
211 optimal_value: serde_json::json!(true),
212 }]
213}
214
215#[cfg(test)]
216#[path = "../../unit_tests/models/misc/string_to_string_correction.rs"]
217mod tests;