Skip to main content

problemreductions/rules/
stringtostringcorrection_ilp.rs

1//! Reduction from StringToStringCorrection to ILP (Integer Linear Programming).
2//!
3//! A time-expanded ILP with state variables z_{t,p,i} tracking token positions,
4//! emptiness bits e_{t,p}, and operation selectors (delete, swap, no-op) at
5//! each of K stages.
6
7use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
8use crate::models::misc::StringToStringCorrection;
9use crate::reduction;
10use crate::rules::traits::{ReduceTo, ReductionResult};
11
12/// Result of reducing StringToStringCorrection to ILP.
13#[derive(Debug, Clone)]
14pub struct ReductionSTSCToILP {
15    target: ILP<bool>,
16    n: usize,
17    bound: usize,
18}
19
20// Index helper functions (free functions to avoid `Self::` ambiguity in trait impls).
21fn idx_z(n: usize, t: usize, p: usize, i: usize) -> usize {
22    t * n * n + p * n + i
23}
24
25fn idx_e(n: usize, k: usize, t: usize, p: usize) -> usize {
26    (k + 1) * n * n + t * n + p
27}
28
29fn idx_d(n: usize, k: usize, t: usize, j: usize) -> usize {
30    (k + 1) * (n * n + n) + (t - 1) * n + j
31}
32
33fn idx_s(n: usize, k: usize, t: usize, j: usize) -> usize {
34    let nm1 = n.saturating_sub(1);
35    (k + 1) * (n * n + n) + k * n + (t - 1) * nm1 + j
36}
37
38fn idx_nu(n: usize, k: usize, t: usize) -> usize {
39    let nm1 = n.saturating_sub(1);
40    (k + 1) * (n * n + n) + k * n + k * nm1 + (t - 1)
41}
42
43fn total_vars(n: usize, k: usize) -> usize {
44    let nm1 = n.saturating_sub(1);
45    (k + 1) * n * n + (k + 1) * n + k * n + k * nm1 + k
46}
47
48impl ReductionResult for ReductionSTSCToILP {
49    type Source = StringToStringCorrection;
50    type Target = ILP<bool>;
51
52    fn target_problem(&self) -> &ILP<bool> {
53        &self.target
54    }
55
56    /// Extract operation sequence from ILP solution.
57    fn extract_solution(
58        &self,
59        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
60    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
61        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
62
63        Ok({
64            let n = self.n;
65            let k = self.bound;
66            let noop_code = 2 * n;
67
68            if n == 0 {
69                return Ok(vec![noop_code; k]);
70            }
71
72            let nm1 = n.saturating_sub(1);
73            let mut ops = Vec::with_capacity(k);
74
75            for t in 1..=k {
76                // current length at step t-1
77                let current_len = (0..n)
78                    .filter(|&p| target_solution[idx_e(n, k, t - 1, p)] == 0)
79                    .count();
80
81                let mut selected = Vec::new();
82                if target_solution[idx_nu(n, k, t)] == 1 {
83                    selected.push(noop_code);
84                }
85                selected.extend((0..n).filter(|&j| target_solution[idx_d(n, k, t, j)] == 1));
86                selected.extend(
87                    (0..nm1)
88                        .filter(|&j| target_solution[idx_s(n, k, t, j)] == 1)
89                        .map(|j| current_len + j),
90                );
91                match selected.as_slice() {
92                    [operation] => ops.push(*operation),
93                    [] => {
94                        return Err(crate::rules::ExtractionError::invalid(format!(
95                            "edit step {t} has no selected operation"
96                        )))
97                    }
98                    _ => {
99                        return Err(crate::rules::ExtractionError::invalid(format!(
100                            "edit step {t} has multiple selected operations"
101                        )))
102                    }
103                }
104            }
105            ops
106        })
107    }
108}
109
110#[reduction(
111    transform = upper_bound {
112        num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound",
113        num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound",
114    },
115    unavailable = {
116        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
117    }
118)]
119impl ReduceTo<ILP<bool>> for StringToStringCorrection {
120    type Result = ReductionSTSCToILP;
121
122    #[allow(clippy::needless_range_loop)]
123    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
124        let n = self.source_length();
125        let m = self.target_length();
126        let k = self.bound();
127        let source = self.source();
128        let target = self.target();
129
130        // If infeasible by length check, return trivially infeasible ILP
131        if m > n || m < n.saturating_sub(k) {
132            return Ok(ReductionSTSCToILP {
133                target: ILP::new(
134                    0,
135                    vec![LinearConstraint::le(vec![], -1)],
136                    vec![],
137                    ObjectiveSense::Minimize,
138                )
139                .map_err(Self::target_construction)?,
140                n,
141                bound: k,
142            });
143        }
144
145        // n == 0 edge case: source and target both empty, all no-ops
146        if n == 0 {
147            let nv = k;
148            let mut constraints = Vec::new();
149            for t in 1..=k {
150                constraints.push(LinearConstraint::eq(vec![(t - 1, 1)], 1));
151            }
152            return Ok(ReductionSTSCToILP {
153                target: ILP::new(nv, constraints, vec![], ObjectiveSense::Minimize)
154                    .map_err(Self::target_construction)?,
155                n,
156                bound: k,
157            });
158        }
159
160        let nm1 = n.saturating_sub(1);
161        let nv = total_vars(n, k);
162
163        let mut constraints = Vec::new();
164
165        // === State validity ===
166
167        // e_{t,p} + Σ_i z_{t,p,i} = 1  ∀ t,p
168        for t in 0..=k {
169            for p in 0..n {
170                let mut terms = vec![(idx_e(n, k, t, p), 1)];
171                for i in 0..n {
172                    terms.push((idx_z(n, t, p, i), 1));
173                }
174                constraints.push(LinearConstraint::eq(terms, 1));
175            }
176        }
177
178        // Σ_p z_{t,p,i} <= 1  ∀ t,i
179        for t in 0..=k {
180            for i in 0..n {
181                let terms: Vec<(usize, i64)> = (0..n).map(|p| (idx_z(n, t, p, i), 1)).collect();
182                constraints.push(LinearConstraint::le(terms, 1));
183            }
184        }
185
186        // e_{t,p} <= e_{t,p+1}  ∀ t, p < n-1
187        for t in 0..=k {
188            for p in 0..nm1 {
189                constraints.push(LinearConstraint::le(
190                    vec![(idx_e(n, k, t, p), 1), (idx_e(n, k, t, p + 1), -1)],
191                    0,
192                ));
193            }
194        }
195
196        // === Initial state ===
197        for p in 0..n {
198            constraints.push(LinearConstraint::eq(vec![(idx_z(n, 0, p, p), 1)], 1));
199            for i in 0..n {
200                if i != p {
201                    constraints.push(LinearConstraint::eq(vec![(idx_z(n, 0, p, i), 1)], 0));
202                }
203            }
204            constraints.push(LinearConstraint::eq(vec![(idx_e(n, k, 0, p), 1)], 0));
205        }
206
207        // === Operation choice ===
208        for t in 1..=k {
209            let mut terms = Vec::new();
210            for j in 0..n {
211                terms.push((idx_d(n, k, t, j), 1));
212            }
213            for j in 0..nm1 {
214                terms.push((idx_s(n, k, t, j), 1));
215            }
216            terms.push((idx_nu(n, k, t), 1));
217            constraints.push(LinearConstraint::eq(terms, 1));
218        }
219
220        // Legality
221        for t in 1..=k {
222            for j in 0..n {
223                constraints.push(LinearConstraint::le(
224                    vec![(idx_d(n, k, t, j), 1), (idx_e(n, k, t - 1, j), 1)],
225                    1,
226                ));
227            }
228            for j in 0..nm1 {
229                constraints.push(LinearConstraint::le(
230                    vec![(idx_s(n, k, t, j), 1), (idx_e(n, k, t - 1, j), 1)],
231                    1,
232                ));
233                constraints.push(LinearConstraint::le(
234                    vec![(idx_s(n, k, t, j), 1), (idx_e(n, k, t - 1, j + 1), 1)],
235                    1,
236                ));
237            }
238        }
239
240        // === State-update (M=1 big-M) ===
241        for t in 1..=k {
242            for p in 0..n {
243                for i in 0..n {
244                    // No-op
245                    constraints.push(LinearConstraint::le(
246                        vec![
247                            (idx_z(n, t, p, i), 1),
248                            (idx_z(n, t - 1, p, i), -1),
249                            (idx_nu(n, k, t), 1),
250                        ],
251                        1,
252                    ));
253                    constraints.push(LinearConstraint::le(
254                        vec![
255                            (idx_z(n, t - 1, p, i), 1),
256                            (idx_z(n, t, p, i), -1),
257                            (idx_nu(n, k, t), 1),
258                        ],
259                        1,
260                    ));
261
262                    // Delete at position j
263                    for j in 0..n {
264                        if p < j {
265                            // Before deleted position: unchanged
266                            constraints.push(LinearConstraint::le(
267                                vec![
268                                    (idx_z(n, t, p, i), 1),
269                                    (idx_z(n, t - 1, p, i), -1),
270                                    (idx_d(n, k, t, j), 1),
271                                ],
272                                1,
273                            ));
274                            constraints.push(LinearConstraint::le(
275                                vec![
276                                    (idx_z(n, t - 1, p, i), 1),
277                                    (idx_z(n, t, p, i), -1),
278                                    (idx_d(n, k, t, j), 1),
279                                ],
280                                1,
281                            ));
282                        } else if p + 1 < n {
283                            // j <= p < n-1: shift from p+1
284                            constraints.push(LinearConstraint::le(
285                                vec![
286                                    (idx_z(n, t, p, i), 1),
287                                    (idx_z(n, t - 1, p + 1, i), -1),
288                                    (idx_d(n, k, t, j), 1),
289                                ],
290                                1,
291                            ));
292                            constraints.push(LinearConstraint::le(
293                                vec![
294                                    (idx_z(n, t - 1, p + 1, i), 1),
295                                    (idx_z(n, t, p, i), -1),
296                                    (idx_d(n, k, t, j), 1),
297                                ],
298                                1,
299                            ));
300                        } else {
301                            // p == n-1: last slot must be empty
302                            constraints.push(LinearConstraint::le(
303                                vec![(idx_z(n, t, n - 1, i), 1), (idx_d(n, k, t, j), 1)],
304                                1,
305                            ));
306                        }
307                    }
308
309                    // Swap at position j
310                    for j in 0..nm1 {
311                        if p != j && p != j + 1 {
312                            constraints.push(LinearConstraint::le(
313                                vec![
314                                    (idx_z(n, t, p, i), 1),
315                                    (idx_z(n, t - 1, p, i), -1),
316                                    (idx_s(n, k, t, j), 1),
317                                ],
318                                1,
319                            ));
320                            constraints.push(LinearConstraint::le(
321                                vec![
322                                    (idx_z(n, t - 1, p, i), 1),
323                                    (idx_z(n, t, p, i), -1),
324                                    (idx_s(n, k, t, j), 1),
325                                ],
326                                1,
327                            ));
328                        } else if p == j {
329                            constraints.push(LinearConstraint::le(
330                                vec![
331                                    (idx_z(n, t, j, i), 1),
332                                    (idx_z(n, t - 1, j + 1, i), -1),
333                                    (idx_s(n, k, t, j), 1),
334                                ],
335                                1,
336                            ));
337                            constraints.push(LinearConstraint::le(
338                                vec![
339                                    (idx_z(n, t - 1, j + 1, i), 1),
340                                    (idx_z(n, t, j, i), -1),
341                                    (idx_s(n, k, t, j), 1),
342                                ],
343                                1,
344                            ));
345                        } else {
346                            // p == j+1
347                            constraints.push(LinearConstraint::le(
348                                vec![
349                                    (idx_z(n, t, j + 1, i), 1),
350                                    (idx_z(n, t - 1, j, i), -1),
351                                    (idx_s(n, k, t, j), 1),
352                                ],
353                                1,
354                            ));
355                            constraints.push(LinearConstraint::le(
356                                vec![
357                                    (idx_z(n, t - 1, j, i), 1),
358                                    (idx_z(n, t, j + 1, i), -1),
359                                    (idx_s(n, k, t, j), 1),
360                                ],
361                                1,
362                            ));
363                        }
364                    }
365                }
366            }
367        }
368
369        // === Final state equals target ===
370        for p in 0..m {
371            let terms: Vec<(usize, i64)> = (0..n)
372                .filter(|&i| source[i] == target[p])
373                .map(|i| (idx_z(n, k, p, i), 1))
374                .collect();
375            constraints.push(LinearConstraint::eq(terms, 1));
376        }
377        for p in m..n {
378            constraints.push(LinearConstraint::eq(vec![(idx_e(n, k, k, p), 1)], 1));
379        }
380
381        let target_ilp = ILP::new(nv, constraints, vec![], ObjectiveSense::Minimize)
382            .map_err(Self::target_construction)?;
383        Ok(ReductionSTSCToILP {
384            target: target_ilp,
385            n,
386            bound: k,
387        })
388    }
389}
390
391#[cfg(feature = "example-db")]
392pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
393    use crate::export::SolutionPair;
394    vec![crate::example_db::specs::RuleExampleSpec {
395        id: "stringtostringcorrection_to_ilp",
396        build: || {
397            // source=[0,1,0], target=[1,0], bound=1 (delete position 0)
398            let source = StringToStringCorrection::new(2, vec![0, 1, 0], vec![1, 0], 1);
399            let reduction: ReductionSTSCToILP =
400                ReduceTo::<ILP<bool>>::reduce_to(&source).expect("reduction should succeed");
401            let target_config = {
402                let ilp_solver = crate::solvers::ILPSolver::new();
403                ilp_solver
404                    .solve(reduction.target_problem())
405                    .expect("ILP should be solvable")
406            };
407            let source_config = reduction.extract_solution(&target_config).unwrap();
408            crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
409                source,
410                SolutionPair {
411                    source_config: serde_json::to_value(source_config)
412                        .expect("solution serialization must succeed"),
413                    target_config: serde_json::to_value(target_config)
414                        .expect("solution serialization must succeed"),
415                },
416            )
417        },
418    }]
419}
420
421#[cfg(test)]
422#[path = "../unit_tests/rules/stringtostringcorrection_ilp.rs"]
423mod tests;