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