Skip to main content

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::{CreateSpec, 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        category: crate::registry::ProblemCategory::Misc,
28        module_path: module_path!(),
29        description: "Derive target string from source using at most K deletions and adjacent swaps",
30        fields: StringToStringCorrectionCreateSpec::FIELDS,
31    }
32}
33
34/// The String-to-String Correction problem.
35///
36/// Given an alphabet of size `a`, a source string `s` over `{0, ..., a-1}`,
37/// a target string `t` over the same alphabet, and a bound `K`, determine
38/// whether `t` can be obtained from `s` by applying at most `K` operations,
39/// where each operation is either a character deletion or a swap of two
40/// adjacent characters.
41///
42/// # Representation
43///
44/// The configuration is a vector of length `K`. For a source string of
45/// length `n`, each entry is in `{0, ..., 2n}`:
46/// - Values `0..current_len` delete the character at that index in the
47///   current working string.
48/// - Values `current_len..2n` swap the character at position
49///   `value - current_len` with its right neighbor.
50/// - Value `2n` is a no-op (skip this slot).
51///
52/// The domain size per slot is fixed at `2n + 1` regardless of how
53/// deletions shorten the working string; as the working string shrinks,
54/// some encodings that were valid before may become invalid.
55///
56/// # Example
57///
58/// ```
59/// use problemreductions::models::misc::StringToStringCorrection;
60/// use problemreductions::{Problem, BruteForce};
61///
62/// // source = [0,1,2,3,1,0], target = [0,1,3,2,1], bound = 2
63/// let problem = StringToStringCorrection::new(4, vec![0,1,2,3,1,0], vec![0,1,3,2,1], 2);
64/// let solver = BruteForce::new();
65/// let solution = solver.solve(&problem).unwrap();
66/// assert!(solution.is_some());
67/// ```
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(try_from = "StringToStringCorrectionData")]
70pub struct StringToStringCorrection {
71    alphabet_size: usize,
72    source: Vec<usize>,
73    target: Vec<usize>,
74    bound: usize,
75}
76
77#[derive(Deserialize)]
78struct StringToStringCorrectionData {
79    alphabet_size: usize,
80    source: Vec<usize>,
81    target: Vec<usize>,
82    bound: usize,
83}
84
85impl TryFrom<StringToStringCorrectionData> for StringToStringCorrection {
86    type Error = crate::registry::ConstructionError;
87
88    fn try_from(data: StringToStringCorrectionData) -> Result<Self, Self::Error> {
89        Self::try_new(data.alphabet_size, data.source, data.target, data.bound)
90    }
91}
92
93#[derive(Debug, Deserialize, crate::CreateSpec)]
94struct StringToStringCorrectionCreateSpec {
95    /// Optional alphabet size; omitted values are inferred from both strings.
96    alphabet_size: Option<usize>,
97    /// Source string.
98    #[create(codec = "comma-separated")]
99    source_string: Vec<usize>,
100    /// Target string.
101    #[create(codec = "comma-separated")]
102    target_string: Vec<usize>,
103    /// Maximum number of correction operations.
104    bound: usize,
105}
106
107impl TryFrom<StringToStringCorrectionCreateSpec> for StringToStringCorrection {
108    type Error = crate::registry::ConstructionError;
109
110    fn try_from(spec: StringToStringCorrectionCreateSpec) -> Result<Self, Self::Error> {
111        let inferred_alphabet_size = spec
112            .source_string
113            .iter()
114            .chain(&spec.target_string)
115            .copied()
116            .max()
117            .map(|symbol| {
118                symbol
119                    .checked_add(1)
120                    .ok_or_else(|| "inferred alphabet size overflows usize".to_string())
121            })
122            .transpose()?
123            .unwrap_or(0);
124        let alphabet_size = spec.alphabet_size.unwrap_or(inferred_alphabet_size);
125        Self::try_new(
126            alphabet_size,
127            spec.source_string,
128            spec.target_string,
129            spec.bound,
130        )
131    }
132}
133
134impl StringToStringCorrection {
135    /// Create a new StringToStringCorrection instance.
136    ///
137    /// # Panics
138    ///
139    /// Panics if `alphabet_size` is 0 when the source or target string is
140    /// non-empty, or if any symbol in `source` or `target` is
141    /// `>= alphabet_size`.
142    pub fn new(alphabet_size: usize, source: Vec<usize>, target: Vec<usize>, bound: usize) -> Self {
143        Self::try_new(alphabet_size, source, target, bound)
144            .unwrap_or_else(|error| panic!("{error}"))
145    }
146
147    fn try_new(
148        alphabet_size: usize,
149        source: Vec<usize>,
150        target: Vec<usize>,
151        bound: usize,
152    ) -> Result<Self, crate::registry::ConstructionError> {
153        if !(alphabet_size > 0 || (source.is_empty() && target.is_empty())) {
154            return Err("alphabet_size must be > 0 when source or target is non-empty".into());
155        }
156        if !(source.iter().all(|&s| s < alphabet_size)) {
157            return Err("all source symbols must be < alphabet_size".into());
158        }
159        if !(target.iter().all(|&s| s < alphabet_size)) {
160            return Err("all target symbols must be < alphabet_size".into());
161        }
162        Ok(Self {
163            alphabet_size,
164            source,
165            target,
166            bound,
167        })
168    }
169
170    /// Returns the alphabet size.
171    pub fn alphabet_size(&self) -> usize {
172        self.alphabet_size
173    }
174
175    /// Returns the source string.
176    pub fn source(&self) -> &[usize] {
177        &self.source
178    }
179
180    /// Returns the target string.
181    pub fn target(&self) -> &[usize] {
182        &self.target
183    }
184
185    /// Returns the operation bound.
186    pub fn bound(&self) -> usize {
187        self.bound
188    }
189
190    /// Returns the length of the source string.
191    pub fn source_length(&self) -> usize {
192        self.source.len()
193    }
194
195    /// Returns the length of the target string.
196    pub fn target_length(&self) -> usize {
197        self.target.len()
198    }
199}
200
201impl Problem for StringToStringCorrection {
202    const NAME: &'static str = "StringToStringCorrection";
203    type Solution = Vec<usize>;
204    type Value = crate::types::Or;
205
206    crate::problem_parameters![("bound", bound), ("source_length", source_length),];
207
208    fn variant() -> Vec<(&'static str, &'static str)> {
209        crate::variant_params![]
210    }
211
212    fn evaluate(
213        &self,
214        config: &Self::Solution,
215    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
216        Ok({
217            crate::types::Or({
218                if config.len() != self.bound {
219                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
220                        "edit-program length does not match the operation bound".into(),
221                    ));
222                }
223                if self.target.len() > self.source.len()
224                    || self.target.len() < self.source.len().saturating_sub(self.bound)
225                {
226                    return Ok(crate::types::Or(false));
227                }
228                let n = self.source.len();
229                let domain = 2 * n + 1;
230                if config.iter().any(|&v| v >= domain) {
231                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
232                        "edit program contains an out-of-range operation".into(),
233                    ));
234                }
235                let noop = 2 * n;
236                let mut working = self.source.clone();
237                for &op in config {
238                    if op == noop {
239                        // no-op
240                        continue;
241                    }
242                    let current_len = working.len();
243                    if op < current_len {
244                        // delete at index op
245                        working.remove(op);
246                    } else {
247                        let swap_pos = op - current_len;
248                        if swap_pos + 1 < current_len {
249                            working.swap(swap_pos, swap_pos + 1);
250                        } else {
251                            // invalid operation for current string state
252                            return Ok(crate::types::Or(false));
253                        }
254                    }
255                }
256                working == self.target
257            })
258        })
259    }
260}
261
262impl crate::solvers::BruteForceProblem for StringToStringCorrection {
263    fn dimensions(&self) -> Vec<usize> {
264        vec![2 * self.source.len() + 1; self.bound]
265    }
266}
267
268crate::declare_variants! {
269    default StringToStringCorrection => "(2 * source_length + 1) ^ bound" create StringToStringCorrectionCreateSpec,
270}
271
272crate::register_brute_force! {
273    StringToStringCorrection,
274}
275
276#[cfg(feature = "example-db")]
277pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
278    vec![crate::example_db::specs::ModelExampleSpec {
279        id: "string_to_string_correction",
280        // source has length 6. Domain = 2*6+1 = 13. No-op = 12.
281        // First operation: swap at positions 2,3 → value = 6 + 2 = 8
282        // Second operation: delete at position 5
283        instance: Box::new(StringToStringCorrection::new(
284            4,
285            vec![0, 1, 2, 3, 1, 0],
286            vec![0, 1, 3, 2, 1],
287            2,
288        )),
289        optimal_config: serde_json::json!(vec![8, 5]),
290        optimal_value: serde_json::json!(true),
291    }]
292}
293
294#[cfg(test)]
295#[path = "../../unit_tests/models/misc/string_to_string_correction.rs"]
296mod tests;