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