Skip to main content

problemreductions/models/misc/
minimum_external_macro_data_compression.rs

1//! Minimum External Macro Data Compression problem implementation.
2//!
3//! Given an alphabet Sigma, a string s in Sigma*, and a pointer cost h,
4//! find a dictionary string D and compressed string C minimizing the total
5//! cost |D| + |C| + (h-1) * (number of pointer occurrences in D and C),
6//! such that s can be reconstructed from C by replacing pointers with their
7//! referenced substrings of D.
8//!
9//! The configuration uses 2*|s| slots: |s| slots for D (dictionary) and |s|
10//! slots for C (compressed string). D-slots use alphabet symbols or empty.
11//! C-slots use alphabet symbols, pointers into D (start, len), or empty.
12//! D is restricted to be pointer-free (pure alphabet string).
13//!
14//! This problem is NP-hard (Storer, 1977; Storer & Szymanski, 1978).
15//! Reference: Garey & Johnson A4 SR22.
16
17use crate::registry::{FieldInfo, ProblemSchemaEntry};
18use crate::traits::Problem;
19use crate::types::Min;
20use serde::{Deserialize, Serialize};
21
22inventory::submit! {
23    ProblemSchemaEntry {
24        name: "MinimumExternalMacroDataCompression",
25        display_name: "Minimum External Macro Data Compression",
26        aliases: &[],
27        dimensions: &[],
28        category: crate::registry::ProblemCategory::Misc,
29        module_path: module_path!(),
30        description: "Find minimum-cost compression using an external dictionary and compressed string with pointers",
31        fields: &[
32            FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the alphabet (symbols indexed 0..alphabet_size)" },
33            FieldInfo { name: "string", type_name: "Vec<usize>", description: "Source string as symbol indices" },
34            FieldInfo { name: "pointer_cost", type_name: "i64", description: "Pointer cost h (each pointer contributes h to the cost)" },
35        ],
36    }
37}
38
39/// Minimum External Macro Data Compression problem.
40///
41/// Given an alphabet of size `k`, a string `s` over `{0, ..., k-1}`, and
42/// a pointer cost `h`, find dictionary string D and compressed string C
43/// that minimize cost = |D| + |C| + (h-1) * (pointer count in C).
44///
45/// # Representation
46///
47/// The configuration is a vector of `2 * string_length` entries:
48/// - First `string_length` entries are D-slots: each is a symbol index
49///   in `{0, ..., alphabet_size-1}` or `alphabet_size` (empty/unused).
50/// - Next `string_length` entries are C-slots: each is:
51///   - A symbol index in `{0, ..., alphabet_size-1}` (literal)
52///   - `alphabet_size` (empty/unused)
53///   - A value in `{alphabet_size+1, ..., alphabet_size + |s|*(|s|+1)/2}`
54///     encoding a pointer (start, len) into D.
55///
56/// D is the prefix of non-empty D-slots. C is the prefix of non-empty C-slots.
57/// The cost is |D| + |C| + (h-1) * (number of pointer symbols in C).
58///
59/// # Example
60///
61/// ```
62/// use problemreductions::models::misc::MinimumExternalMacroDataCompression;
63/// use problemreductions::{Problem, BruteForce};
64///
65/// // Alphabet {a, b}, string "abab", pointer cost h=2
66/// let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2);
67/// let solver = BruteForce::new();
68/// let solution = solver.solve(&problem).unwrap();
69/// assert!(solution.is_some());
70/// ```
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(try_from = "MinimumExternalMacroDataCompressionSerde")]
73pub struct MinimumExternalMacroDataCompression {
74    alphabet_size: usize,
75    string: Vec<usize>,
76    pointer_cost: i64,
77}
78
79#[derive(Deserialize)]
80struct MinimumExternalMacroDataCompressionSerde {
81    alphabet_size: usize,
82    string: Vec<usize>,
83    pointer_cost: i64,
84}
85
86impl TryFrom<MinimumExternalMacroDataCompressionSerde> for MinimumExternalMacroDataCompression {
87    type Error = crate::registry::ConstructionError;
88
89    fn try_from(value: MinimumExternalMacroDataCompressionSerde) -> Result<Self, Self::Error> {
90        if value.alphabet_size == 0 && !value.string.is_empty() {
91            return Err("alphabet_size must be > 0 when the string is non-empty"
92                .to_string()
93                .into());
94        }
95        if value
96            .string
97            .iter()
98            .any(|&symbol| symbol >= value.alphabet_size)
99        {
100            return Err("all symbols must be less than alphabet_size"
101                .to_string()
102                .into());
103        }
104        if value.pointer_cost <= 0 {
105            return Err("pointer_cost must be positive".to_string().into());
106        }
107        Ok(Self {
108            alphabet_size: value.alphabet_size,
109            string: value.string,
110            pointer_cost: value.pointer_cost,
111        })
112    }
113}
114
115impl MinimumExternalMacroDataCompression {
116    /// Create a new MinimumExternalMacroDataCompression instance.
117    ///
118    /// # Panics
119    ///
120    /// Panics if `alphabet_size` is 0 and the string is non-empty, or if
121    /// any symbol in the string is >= `alphabet_size`, or if `pointer_cost` is 0.
122    pub fn new(alphabet_size: usize, string: Vec<usize>, pointer_cost: i64) -> Self {
123        assert!(
124            alphabet_size > 0 || string.is_empty(),
125            "alphabet_size must be > 0 when the string is non-empty"
126        );
127        assert!(
128            string
129                .iter()
130                .all(|&s| s < alphabet_size || alphabet_size == 0),
131            "all symbols must be less than alphabet_size"
132        );
133        assert!(pointer_cost > 0, "pointer_cost must be positive");
134        Self {
135            alphabet_size,
136            string,
137            pointer_cost,
138        }
139    }
140
141    /// Returns the length of the source string.
142    pub fn string_length(&self) -> usize {
143        self.string.len()
144    }
145
146    /// Returns the alphabet size.
147    pub fn alphabet_size(&self) -> usize {
148        self.alphabet_size
149    }
150
151    /// Returns the pointer cost h.
152    pub fn pointer_cost(&self) -> i64 {
153        self.pointer_cost
154    }
155
156    /// Returns the source string.
157    pub fn string(&self) -> &[usize] {
158        &self.string
159    }
160
161    /// Returns the number of valid pointers into D (|s|*(|s|+1)/2).
162    fn num_pointers(&self) -> usize {
163        let n = self.string.len();
164        n * (n + 1) / 2
165    }
166
167    /// Returns the C-slot domain size: alphabet_size + 1 (empty) + num_pointers.
168    fn c_domain_size(&self) -> usize {
169        self.alphabet_size + 1 + self.num_pointers()
170    }
171
172    /// Decode a pointer index (offset from alphabet_size+1) into (start, len)
173    /// in the dictionary. Pointers are enumerated as:
174    /// index 0 -> (0, 1), 1 -> (0, 2), ..., n-1 -> (0, n),
175    /// n -> (1, 1), n+1 -> (1, 2), ..., etc.
176    fn decode_pointer(&self, ptr_idx: usize) -> Option<(usize, usize)> {
177        let n = self.string.len();
178        // Enumerate (start, len) pairs where 0 <= start < n, 1 <= len <= n - start
179        let mut idx = 0;
180        for start in 0..n {
181            let max_len = n - start;
182            if ptr_idx < idx + max_len {
183                let len = ptr_idx - idx + 1;
184                return Some((start, len));
185            }
186            idx += max_len;
187        }
188        None
189    }
190}
191
192impl Problem for MinimumExternalMacroDataCompression {
193    const NAME: &'static str = "MinimumExternalMacroDataCompression";
194    type Solution = Vec<usize>;
195    type Value = Min<i64>;
196
197    crate::problem_parameters![
198        ("alphabet_size", alphabet_size),
199        ("string_length", string_length),
200    ];
201
202    fn variant() -> Vec<(&'static str, &'static str)> {
203        crate::variant_params![]
204    }
205
206    fn evaluate(
207        &self,
208        config: &Self::Solution,
209    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
210        Ok({
211            let n = self.string.len();
212            if config.len() != 2 * n {
213                return Err(crate::traits::EvaluationError::InvalidConfiguration(
214                    "macro encoding length does not match the string".into(),
215                ));
216            }
217
218            // Handle empty string case
219            if n == 0 {
220                return Ok(Min(Some(0)));
221            }
222
223            let empty_d = self.alphabet_size; // empty marker for D-slots
224            let empty_c = self.alphabet_size; // empty marker for C-slots
225
226            // Decode D: prefix of non-empty D-slots
227            let d_slots = &config[..n];
228            let d_len = d_slots.iter().position(|&v| v == empty_d).unwrap_or(n);
229
230            // Verify contiguous: all after first empty must be empty
231            for &v in &d_slots[d_len..] {
232                if v != empty_d {
233                    return Ok(Min(None));
234                }
235            }
236
237            // Verify D symbols are valid alphabet symbols
238            let d_str: Vec<usize> = d_slots[..d_len].to_vec();
239            if d_str.iter().any(|&v| v >= self.alphabet_size) {
240                return Ok(Min(None));
241            }
242
243            // Decode C: prefix of non-empty C-slots
244            let c_slots = &config[n..];
245            let c_len = c_slots.iter().position(|&v| v == empty_c).unwrap_or(n);
246
247            // Verify contiguous: all after first empty must be empty
248            for &v in &c_slots[c_len..] {
249                if v != empty_c {
250                    return Ok(Min(None));
251                }
252            }
253
254            // Decode C into a sequence of symbols, counting pointers
255            let mut decoded = Vec::new();
256            let mut pointer_count: usize = 0;
257
258            for &v in &c_slots[..c_len] {
259                if v < self.alphabet_size {
260                    // Literal symbol
261                    decoded.push(v);
262                } else if v > self.alphabet_size {
263                    // Pointer into D
264                    let ptr_idx = v - (self.alphabet_size + 1);
265                    if let Some((start, len)) = self.decode_pointer(ptr_idx) {
266                        // Pointer must reference valid portion of D
267                        if start + len > d_len {
268                            return Ok(Min(None));
269                        }
270                        decoded.extend_from_slice(&d_str[start..start + len]);
271                        pointer_count += 1;
272                    } else {
273                        return Ok(Min(None));
274                    }
275                } else {
276                    // v == empty_c, but we already filtered those out
277                    return Ok(Min(None));
278                }
279            }
280
281            // Check decoded string matches the source string
282            if decoded != self.string {
283                return Ok(Min(None));
284            }
285
286            // Compute cost: |D| + |C| + (h-1) * pointer_count
287            let d_len = i64::try_from(d_len).map_err(|_| {
288                crate::traits::EvaluationError::IntegerOverflow(
289                    "converting the dictionary length to i64".to_string(),
290                )
291            })?;
292            let c_len = i64::try_from(c_len).map_err(|_| {
293                crate::traits::EvaluationError::IntegerOverflow(
294                    "converting the compressed-string length to i64".to_string(),
295                )
296            })?;
297            let pointer_count = i64::try_from(pointer_count).map_err(|_| {
298                crate::traits::EvaluationError::IntegerOverflow(
299                    "converting the pointer count to i64".to_string(),
300                )
301            })?;
302            let pointer_cost = self
303                .pointer_cost
304                .checked_sub(1)
305                .and_then(|cost| cost.checked_mul(pointer_count))
306                .ok_or_else(|| {
307                    crate::traits::EvaluationError::IntegerOverflow(
308                        "computing the external macro pointer cost".to_string(),
309                    )
310                })?;
311            let cost = d_len
312                .checked_add(c_len)
313                .and_then(|cost| cost.checked_add(pointer_cost))
314                .ok_or_else(|| {
315                    crate::traits::EvaluationError::IntegerOverflow(
316                        "computing the external macro compression cost".to_string(),
317                    )
318                })?;
319            Min(Some(cost))
320        })
321    }
322}
323
324impl crate::solvers::BruteForceProblem for MinimumExternalMacroDataCompression {
325    fn dimensions(&self) -> Vec<usize> {
326        let n = self.string.len();
327        let d_domain = self.alphabet_size + 1; // symbols + empty
328        let c_domain = self.c_domain_size(); // symbols + empty + pointers
329        let mut dims = vec![d_domain; n]; // D-slots
330        dims.extend(vec![c_domain; n]); // C-slots
331        dims
332    }
333}
334
335crate::declare_variants! {
336    default MinimumExternalMacroDataCompression => "(alphabet_size + 1) ^ string_length * (alphabet_size + 1 + string_length * (string_length + 1) / 2) ^ string_length",
337}
338
339crate::register_brute_force! {
340    MinimumExternalMacroDataCompression,
341}
342
343#[cfg(feature = "example-db")]
344pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
345    // Issue #441 example: alphabet {a,b,c,d,e,f} (6), s="abcdefabcdefabcdef" (18), h=2.
346    // Optimal: D="abcdef"(6), C = ptr(0,6) ptr(0,6) ptr(0,6), cost = 6+3+(2-1)*3 = 12.
347    // Solved via ILP reduction (brute force infeasible at this size).
348    //
349    // Config encoding (2*18 = 36 slots):
350    // D-slots: [0,1,2,3,4,5, 6,6,...,6] (6 symbols + 12 empty, empty=alphabet_size=6)
351    // C-slots: [ptr(0,6), ptr(0,6), ptr(0,6), 6,6,...,6] (3 pointers + 15 empty)
352    // ptr(0,6) index: start=0, len=6 → index 5 → encoded as 6+1+5 = 12
353    let s: Vec<usize> = (0..6).cycle().take(18).collect();
354    let mut optimal_config = vec![0, 1, 2, 3, 4, 5];
355    optimal_config.extend(vec![6; 12]); // empty D-slots
356    optimal_config.extend(vec![12, 12, 12]); // 3 pointers to D[0..6]
357    optimal_config.extend(vec![6; 15]); // empty C-slots
358    vec![crate::example_db::specs::ModelExampleSpec {
359        id: "minimum_external_macro_data_compression",
360        instance: Box::new(MinimumExternalMacroDataCompression::new(6, s, 2)),
361        optimal_config: serde_json::to_value(optimal_config)
362            .expect("solution serialization must succeed"),
363        optimal_value: serde_json::json!(12),
364    }]
365}
366
367#[cfg(test)]
368#[path = "../../unit_tests/models/misc/minimum_external_macro_data_compression.rs"]
369mod tests;