Skip to main content

problemreductions/rules/
minimuminternalmacrodatacompression_ilp.rs

1//! Reduction from MinimumInternalMacroDataCompression to ILP (Integer Linear Programming).
2//!
3//! The IMDC problem is formulated as a binary ILP using a flow-on-DAG partition
4//! model for the compressed string C, where pointers reference earlier segments
5//! within C itself.
6//!
7//! **Variables (all binary):**
8//! - `lit[i]`: source position i is covered by a literal in C (i=0..n-1)
9//! - `ptr[i][l][r]`: segment [i, i+l) of the source is covered by a pointer in C
10//!   that copies from source position r (the first l characters of the decoded
11//!   output starting at source position r must equal s[i..i+l])
12//! - Flow conservation ensures positions 0..n are partitioned into segments.
13//!
14//! **Objective:** minimize |C| + (h−1) × pointer_count
15//! = (sum lit[i]) + (sum ptr[i][l][r]) + (h−1) × (sum ptr[i][l][r])
16//! = (sum lit[i]) + h × (sum ptr[i][l][r])
17
18use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
19use crate::models::misc::MinimumInternalMacroDataCompression;
20use crate::reduction;
21use crate::rules::traits::{ReduceTo, ReductionResult};
22
23/// Index layout for ILP variables.
24#[derive(Debug, Clone)]
25struct VarLayout {
26    n: usize,
27    /// Offset of lit[i] block: index = lit_offset + i
28    lit_offset: usize,
29    /// Offset of ptr variables, stored as a flat list.
30    /// Each entry in `ptr_triples` is (i, l, r) and its var index = ptr_offset + idx.
31    ptr_offset: usize,
32    /// The (i, l, r) triples in order.
33    ptr_triples: Vec<(usize, usize, usize)>,
34    /// Total number of variables.
35    total_vars: usize,
36}
37
38impl VarLayout {
39    fn new(n: usize, source_string: &[usize]) -> Self {
40        let lit_offset = 0;
41        let ptr_offset = lit_offset + n;
42
43        // Enumerate all valid (i, l, r) triples where:
44        // - i is the start position in the source (0..n)
45        // - l is the segment length (1..n-i)
46        // - r is the reference position in the source (0..i), meaning
47        //   the pointer copies from source[r..r+l] which must equal source[i..i+l]
48        //   AND r < i (pointer references earlier decoded content)
49        let mut ptr_triples = Vec::new();
50        for i in 0..n {
51            for l in 1..=(n - i) {
52                for r in 0..i {
53                    // The pointer copies from decoded[r..r+l]. With non-overlapping
54                    // semantics, decoded has exactly i characters before this pointer,
55                    // so we need r + l <= i.
56                    if r + l <= i
57                        && r + l <= n
58                        && source_string[r..r + l] == source_string[i..i + l]
59                    {
60                        ptr_triples.push((i, l, r));
61                    }
62                }
63            }
64        }
65
66        let total_vars = ptr_offset + ptr_triples.len();
67        Self {
68            n,
69            lit_offset,
70            ptr_offset,
71            ptr_triples,
72            total_vars,
73        }
74    }
75
76    fn lit_var(&self, i: usize) -> usize {
77        self.lit_offset + i
78    }
79}
80
81/// Result of reducing MinimumInternalMacroDataCompression to ILP.
82#[derive(Debug, Clone)]
83pub struct ReductionIMDCToILP {
84    target: ILP<bool>,
85    layout: VarLayout,
86    source_string: Vec<usize>,
87    alphabet_size: usize,
88}
89
90impl ReductionResult for ReductionIMDCToILP {
91    type Source = MinimumInternalMacroDataCompression;
92    type Target = ILP<bool>;
93
94    fn target_problem(&self) -> &ILP<bool> {
95        &self.target
96    }
97
98    fn extract_solution(
99        &self,
100        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
101    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
102        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
103
104        Ok({
105            let n = self.layout.n;
106            let k = self.alphabet_size;
107            let eos = k; // end-of-string marker
108
109            // First pass: collect segments and build source-to-compressed-position map.
110            // source_to_c_pos[i] = compressed position that covers source position i.
111            let mut source_to_c_pos = vec![0usize; n];
112            let mut segments: Vec<(usize, usize, Option<usize>)> = Vec::new(); // (source_start, len, ref_source_pos)
113            let mut c_pos = 0;
114            let mut pos = 0;
115
116            while pos < n {
117                if target_solution[self.layout.lit_var(pos)] == 1 {
118                    source_to_c_pos[pos] = c_pos;
119                    segments.push((pos, 1, None));
120                    c_pos += 1;
121                    pos += 1;
122                    continue;
123                }
124                let mut found = false;
125                for (idx, &(i, l, r)) in self.layout.ptr_triples.iter().enumerate() {
126                    if i == pos && target_solution[self.layout.ptr_offset + idx] == 1 {
127                        for offset in 0..l {
128                            source_to_c_pos[pos + offset] = c_pos;
129                        }
130                        segments.push((pos, l, Some(r)));
131                        c_pos += 1;
132                        pos += l;
133                        found = true;
134                        break;
135                    }
136                }
137                if !found {
138                    pos += 1;
139                }
140            }
141
142            // Second pass: build config using source_to_c_pos for pointer references
143            let mut config = vec![eos; n];
144            for (idx, &(src_start, _len, ref_pos)) in segments.iter().enumerate() {
145                match ref_pos {
146                    None => {
147                        config[idx] = self.source_string[src_start];
148                    }
149                    Some(r) => {
150                        // Pointer references source position r, which is at
151                        // compressed position source_to_c_pos[r]
152                        config[idx] = k + 1 + source_to_c_pos[r];
153                    }
154                }
155            }
156
157            config
158        })
159    }
160}
161
162// Non-overlapping length-l references number at most
163// (n-2l+1)(n-2l+2)/2, for 1<=l<=floor(n/2). Their sum is
164// (2n^3+3n^2-2n)/24 for even n and 1/8 less for odd n.
165// n(n-1)(n+3)/12 bounds it for n>=4; final ceilings cover n=0..3.
166// Substring filtering only removes pointers. Every positive-length segment
167// appears in exactly two endpoint rows; empty input has zero variables/terms.
168#[reduction(transform = upper_bound {
169    max_constraint_magnitude_bits = "1",
170    num_vars = "string_len + string_len * (string_len - 1) * (string_len + 3) / 12",
171    num_constraints = "string_len + 1",
172    num_nonzeros = "2 * string_len + string_len * (string_len - 1) * (string_len + 3) / 6",
173})]
174impl ReduceTo<ILP<bool>> for MinimumInternalMacroDataCompression {
175    type Result = ReductionIMDCToILP;
176
177    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
178        let n = self.string_len();
179        let k = self.alphabet_size();
180        let h = self.pointer_cost();
181        let s = self.string();
182
183        // Handle empty string
184        if n == 0 {
185            let layout = VarLayout::new(0, s);
186            let target = ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
187                .map_err(Self::target_construction)?;
188            return Ok(ReductionIMDCToILP {
189                target,
190                layout,
191                source_string: vec![],
192                alphabet_size: k,
193            });
194        }
195
196        let layout = VarLayout::new(n, s);
197        let num_vars = layout.total_vars;
198        let mut constraints = Vec::new();
199
200        // Flow conservation on DAG: positions 0..n are nodes.
201        // A segment covers source positions [i, i+l).
202        // Segments: lit[i] covers [i, i+1), ptr[i][l][r] covers [i, i+l).
203        //
204        // Flow constraints:
205        // At node 0: sum of outgoing segments = 1
206        // At node j (1..n-1): sum of incoming = sum of outgoing
207        // At node n: sum of incoming = 1
208
209        let segment_terms = |i: usize, l: usize| -> Vec<(usize, i64)> {
210            let mut terms = Vec::new();
211            if l == 1 {
212                terms.push((layout.lit_var(i), 1));
213            }
214            // All ptr variables for segment (i, l, *)
215            for (idx, &(pi, pl, _)) in layout.ptr_triples.iter().enumerate() {
216                if pi == i && pl == l {
217                    terms.push((layout.ptr_offset + idx, 1));
218                }
219            }
220            terms
221        };
222
223        for node in 0..=n {
224            let mut all_terms: Vec<(usize, i64)> = Vec::new();
225
226            if node == 0 {
227                for l in 1..=n {
228                    all_terms.extend(segment_terms(0, l));
229                }
230                constraints.push(LinearConstraint::eq(all_terms, 1));
231            } else if node == n {
232                for j in 0..n {
233                    let l = n - j;
234                    all_terms.extend(segment_terms(j, l));
235                }
236                constraints.push(LinearConstraint::eq(all_terms, 1));
237            } else {
238                let mut incoming = Vec::new();
239                for j in 0..node {
240                    let l = node - j;
241                    incoming.extend(segment_terms(j, l));
242                }
243                let mut outgoing = Vec::new();
244                for l in 1..=(n - node) {
245                    outgoing.extend(segment_terms(node, l));
246                }
247                for (var, coef) in incoming {
248                    all_terms.push((var, coef));
249                }
250                for (var, coef) in outgoing {
251                    all_terms.push((var, -coef));
252                }
253                constraints.push(LinearConstraint::eq(all_terms, 0));
254            }
255        }
256
257        // Pointer precedence: for ptr[i][l][r], we need r < i (already enforced
258        // by the triple enumeration). Additionally, the content at source[r..r+l]
259        // must equal source[i..i+l] (also enforced by triple enumeration).
260        // No additional constraints needed since we pre-filtered valid triples.
261
262        // Objective: minimize literals + h * pointers
263        // = sum lit[i] + h * sum ptr[i][l][r]
264        // Since each literal contributes 1 to |C| and each pointer contributes
265        // 1 to |C| plus (h-1) to the pointer penalty:
266        // cost = |C| + (h-1)*pointers = (lits + ptrs) + (h-1)*ptrs = lits + h*ptrs
267        let mut objective: Vec<(usize, i64)> = Vec::new();
268        for i in 0..n {
269            objective.push((layout.lit_var(i), 1));
270        }
271        for (idx, _) in layout.ptr_triples.iter().enumerate() {
272            objective.push((layout.ptr_offset + idx, h));
273        }
274
275        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
276            .map_err(Self::target_construction)?;
277
278        Ok(ReductionIMDCToILP {
279            target,
280            layout,
281            source_string: s.to_vec(),
282            alphabet_size: k,
283        })
284    }
285}
286
287#[cfg(feature = "example-db")]
288pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
289    use crate::export::SolutionPair;
290
291    // s = "ab" (len 2), alphabet {a,b} (size 2), h=2
292    // Optimal: uncompressed C="ab", cost = 2
293    // ILP: lit[0]=1, lit[1]=1, no pointers
294    vec![crate::example_db::specs::RuleExampleSpec {
295        id: "minimuminternalmacrodatacompression_to_ilp",
296        build: || {
297            let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2);
298            let reduction =
299                ReduceTo::<ILP<bool>>::reduce_to(&source).expect("reduction should succeed");
300            let layout = &reduction.layout;
301
302            let mut target_config = vec![0_i64; layout.total_vars];
303            target_config[layout.lit_var(0)] = 1;
304            target_config[layout.lit_var(1)] = 1;
305
306            let source_config = reduction.extract_solution(&target_config).unwrap();
307
308            crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
309                source,
310                SolutionPair {
311                    source_config: serde_json::to_value(source_config)
312                        .expect("solution serialization must succeed"),
313                    target_config: serde_json::to_value(target_config)
314                        .expect("solution serialization must succeed"),
315                },
316            )
317        },
318    }]
319}
320
321#[cfg(test)]
322#[path = "../unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs"]
323mod tests;