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#[reduction(
163    transform = upper_bound {
164        num_vars = "string_len + string_len ^ 3",
165        num_constraints = "string_len + 1",
166    },
167    unavailable = {
168        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
169    }
170)]
171impl ReduceTo<ILP<bool>> for MinimumInternalMacroDataCompression {
172    type Result = ReductionIMDCToILP;
173
174    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
175        let n = self.string_len();
176        let k = self.alphabet_size();
177        let h = self.pointer_cost();
178        let s = self.string();
179
180        // Handle empty string
181        if n == 0 {
182            let layout = VarLayout::new(0, s);
183            let target = ILP::new(0, vec![], vec![], ObjectiveSense::Minimize)
184                .map_err(Self::target_construction)?;
185            return Ok(ReductionIMDCToILP {
186                target,
187                layout,
188                source_string: vec![],
189                alphabet_size: k,
190            });
191        }
192
193        let layout = VarLayout::new(n, s);
194        let num_vars = layout.total_vars;
195        let mut constraints = Vec::new();
196
197        // Flow conservation on DAG: positions 0..n are nodes.
198        // A segment covers source positions [i, i+l).
199        // Segments: lit[i] covers [i, i+1), ptr[i][l][r] covers [i, i+l).
200        //
201        // Flow constraints:
202        // At node 0: sum of outgoing segments = 1
203        // At node j (1..n-1): sum of incoming = sum of outgoing
204        // At node n: sum of incoming = 1
205
206        let segment_terms = |i: usize, l: usize| -> Vec<(usize, i64)> {
207            let mut terms = Vec::new();
208            if l == 1 {
209                terms.push((layout.lit_var(i), 1));
210            }
211            // All ptr variables for segment (i, l, *)
212            for (idx, &(pi, pl, _)) in layout.ptr_triples.iter().enumerate() {
213                if pi == i && pl == l {
214                    terms.push((layout.ptr_offset + idx, 1));
215                }
216            }
217            terms
218        };
219
220        for node in 0..=n {
221            let mut all_terms: Vec<(usize, i64)> = Vec::new();
222
223            if node == 0 {
224                for l in 1..=n {
225                    all_terms.extend(segment_terms(0, l));
226                }
227                constraints.push(LinearConstraint::eq(all_terms, 1));
228            } else if node == n {
229                for j in 0..n {
230                    let l = n - j;
231                    all_terms.extend(segment_terms(j, l));
232                }
233                constraints.push(LinearConstraint::eq(all_terms, 1));
234            } else {
235                let mut incoming = Vec::new();
236                for j in 0..node {
237                    let l = node - j;
238                    incoming.extend(segment_terms(j, l));
239                }
240                let mut outgoing = Vec::new();
241                for l in 1..=(n - node) {
242                    outgoing.extend(segment_terms(node, l));
243                }
244                for (var, coef) in incoming {
245                    all_terms.push((var, coef));
246                }
247                for (var, coef) in outgoing {
248                    all_terms.push((var, -coef));
249                }
250                constraints.push(LinearConstraint::eq(all_terms, 0));
251            }
252        }
253
254        // Pointer precedence: for ptr[i][l][r], we need r < i (already enforced
255        // by the triple enumeration). Additionally, the content at source[r..r+l]
256        // must equal source[i..i+l] (also enforced by triple enumeration).
257        // No additional constraints needed since we pre-filtered valid triples.
258
259        // Objective: minimize literals + h * pointers
260        // = sum lit[i] + h * sum ptr[i][l][r]
261        // Since each literal contributes 1 to |C| and each pointer contributes
262        // 1 to |C| plus (h-1) to the pointer penalty:
263        // cost = |C| + (h-1)*pointers = (lits + ptrs) + (h-1)*ptrs = lits + h*ptrs
264        let mut objective: Vec<(usize, i64)> = Vec::new();
265        for i in 0..n {
266            objective.push((layout.lit_var(i), 1));
267        }
268        for (idx, _) in layout.ptr_triples.iter().enumerate() {
269            objective.push((layout.ptr_offset + idx, h));
270        }
271
272        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
273            .map_err(Self::target_construction)?;
274
275        Ok(ReductionIMDCToILP {
276            target,
277            layout,
278            source_string: s.to_vec(),
279            alphabet_size: k,
280        })
281    }
282}
283
284#[cfg(feature = "example-db")]
285pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
286    use crate::export::SolutionPair;
287
288    // s = "ab" (len 2), alphabet {a,b} (size 2), h=2
289    // Optimal: uncompressed C="ab", cost = 2
290    // ILP: lit[0]=1, lit[1]=1, no pointers
291    vec![crate::example_db::specs::RuleExampleSpec {
292        id: "minimuminternalmacrodatacompression_to_ilp",
293        build: || {
294            let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2);
295            let reduction =
296                ReduceTo::<ILP<bool>>::reduce_to(&source).expect("reduction should succeed");
297            let layout = &reduction.layout;
298
299            let mut target_config = vec![0_i64; layout.total_vars];
300            target_config[layout.lit_var(0)] = 1;
301            target_config[layout.lit_var(1)] = 1;
302
303            let source_config = reduction.extract_solution(&target_config).unwrap();
304
305            crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
306                source,
307                SolutionPair {
308                    source_config: serde_json::to_value(source_config)
309                        .expect("solution serialization must succeed"),
310                    target_config: serde_json::to_value(target_config)
311                        .expect("solution serialization must succeed"),
312                },
313            )
314        },
315    }]
316}
317
318#[cfg(test)]
319#[path = "../unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs"]
320mod tests;