Skip to main content

problemreductions/models/misc/
minimum_internal_macro_data_compression.rs

1//! Minimum Internal Macro Data Compression problem implementation.
2//!
3//! Given an alphabet Σ, a string s ∈ Σ*, and a pointer cost h,
4//! find a single compressed string C ∈ (Σ ∪ {pointers})* minimizing the cost
5//! |C| + (h−1) × (number of pointer occurrences in C),
6//! such that s can be obtained from C by resolving all pointer references
7//! within C itself (left-to-right, greedy longest match).
8//!
9//! Unlike external macro compression, there is no separate dictionary — the
10//! compressed string C serves as both dictionary and output.
11//!
12//! This problem is NP-hard (Storer, 1977; Storer & Szymanski, 1978).
13//! Reference: Garey & Johnson A4 SR23.
14
15use crate::registry::{FieldInfo, ProblemSchemaEntry};
16use crate::traits::Problem;
17use crate::types::Min;
18use serde::{Deserialize, Serialize};
19
20inventory::submit! {
21    ProblemSchemaEntry {
22        name: "MinimumInternalMacroDataCompression",
23        display_name: "Minimum Internal Macro Data Compression",
24        aliases: &[],
25        dimensions: &[],
26        category: crate::registry::ProblemCategory::Misc,
27        module_path: module_path!(),
28        description: "Find minimum-cost self-referencing compression of a string with embedded pointers",
29        fields: &[
30            FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the alphabet (symbols indexed 0..alphabet_size)" },
31            FieldInfo { name: "string", type_name: "Vec<usize>", description: "Source string as symbol indices" },
32            FieldInfo { name: "pointer_cost", type_name: "i64", description: "Pointer cost h (each pointer adds h−1 extra to the cost)" },
33        ],
34    }
35}
36
37/// Minimum Internal Macro Data Compression problem.
38///
39/// Given an alphabet of size `k`, a string `s` over `{0, ..., k-1}`, and
40/// a pointer cost `h`, find a compressed string C that minimizes
41/// cost = |C| + (h−1) × (pointer count in C), where C uses itself as both
42/// dictionary and compressed output.
43///
44/// # Representation
45///
46/// The configuration is a vector of `string_len` entries. Each entry is:
47/// - A symbol index in `{0, ..., alphabet_size-1}` (literal)
48/// - `alphabet_size` (end-of-string marker; positions after this are padding)
49/// - A value in `{alphabet_size+1, ..., alphabet_size + string_len}`,
50///   encoding a pointer to C\[v − alphabet_size − 1\] with greedy longest match.
51///
52/// During decoding, pointers are resolved left-to-right. A pointer at position
53/// i referencing position j (where j < i in the decoded output) copies symbols
54/// from the already-decoded output starting at j using greedy longest match.
55///
56/// # Example
57///
58/// ```
59/// use problemreductions::models::misc::MinimumInternalMacroDataCompression;
60/// use problemreductions::{Problem, BruteForce};
61///
62/// // Alphabet {a, b}, string "abab", pointer cost h=2
63/// let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2);
64/// let solver = BruteForce::new();
65/// let solution = solver.solve(&problem).unwrap();
66/// assert!(solution.is_some());
67/// ```
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(try_from = "MinimumInternalMacroDataCompressionSerde")]
70pub struct MinimumInternalMacroDataCompression {
71    alphabet_size: usize,
72    string: Vec<usize>,
73    pointer_cost: i64,
74}
75
76#[derive(Deserialize)]
77struct MinimumInternalMacroDataCompressionSerde {
78    alphabet_size: usize,
79    string: Vec<usize>,
80    pointer_cost: i64,
81}
82
83impl TryFrom<MinimumInternalMacroDataCompressionSerde> for MinimumInternalMacroDataCompression {
84    type Error = crate::registry::ConstructionError;
85
86    fn try_from(value: MinimumInternalMacroDataCompressionSerde) -> Result<Self, Self::Error> {
87        if value.alphabet_size == 0 && !value.string.is_empty() {
88            return Err("alphabet_size must be > 0 when the string is non-empty"
89                .to_string()
90                .into());
91        }
92        if value
93            .string
94            .iter()
95            .any(|&symbol| symbol >= value.alphabet_size)
96        {
97            return Err("all symbols must be less than alphabet_size"
98                .to_string()
99                .into());
100        }
101        if value.pointer_cost <= 0 {
102            return Err("pointer_cost must be positive".to_string().into());
103        }
104        Ok(Self {
105            alphabet_size: value.alphabet_size,
106            string: value.string,
107            pointer_cost: value.pointer_cost,
108        })
109    }
110}
111
112impl MinimumInternalMacroDataCompression {
113    /// Create a new MinimumInternalMacroDataCompression instance.
114    ///
115    /// # Panics
116    ///
117    /// Panics if `alphabet_size` is 0 and the string is non-empty, or if
118    /// any symbol in the string is >= `alphabet_size`, or if `pointer_cost` is 0.
119    pub fn new(alphabet_size: usize, string: Vec<usize>, pointer_cost: i64) -> Self {
120        assert!(
121            alphabet_size > 0 || string.is_empty(),
122            "alphabet_size must be > 0 when the string is non-empty"
123        );
124        assert!(
125            string
126                .iter()
127                .all(|&s| s < alphabet_size || alphabet_size == 0),
128            "all symbols must be less than alphabet_size"
129        );
130        assert!(pointer_cost > 0, "pointer_cost must be positive");
131        Self {
132            alphabet_size,
133            string,
134            pointer_cost,
135        }
136    }
137
138    /// Returns the length of the source string.
139    pub fn string_len(&self) -> usize {
140        self.string.len()
141    }
142
143    /// Returns the alphabet size.
144    pub fn alphabet_size(&self) -> usize {
145        self.alphabet_size
146    }
147
148    /// Returns the pointer cost h.
149    pub fn pointer_cost(&self) -> i64 {
150        self.pointer_cost
151    }
152
153    /// Returns the source string.
154    pub fn string(&self) -> &[usize] {
155        &self.string
156    }
157
158    /// Decode the compressed string C and return the decoded string,
159    /// the active length of C, and the pointer count.
160    /// Returns None if decoding fails (invalid pointer, circular reference, etc.).
161    fn decode(&self, config: &[usize]) -> Option<(Vec<usize>, usize, usize)> {
162        let n = self.string.len();
163        let k = self.alphabet_size;
164        let eos = k; // end-of-string marker
165
166        // Find active length: prefix before first end-of-string marker
167        let active_len = config.iter().position(|&v| v == eos).unwrap_or(n);
168
169        // Verify contiguous: all after first EOS must be EOS or padding
170        for &v in &config[active_len..] {
171            if v != eos {
172                return None;
173            }
174        }
175
176        // Decode left-to-right. A pointer at compressed position c_idx
177        // referencing C[j] copies from the decoded output that existed
178        // before this pointer (no overlapping/runaway copy).
179        let mut decoded = Vec::new();
180        let mut pointer_count: usize = 0;
181
182        for &v in &config[..active_len] {
183            if v < k {
184                // Literal symbol
185                decoded.push(v);
186            } else if v > k {
187                // Pointer: references C[ref_pos] in the compressed string
188                let ref_pos = v - k - 1;
189                if ref_pos >= decoded.len() {
190                    return None; // pointer references undecoded position
191                }
192                // Greedy longest match from decoded[ref_pos..copy_start]
193                // (only pre-existing decoded content, no overlapping copy)
194                let copy_start = decoded.len();
195                let mut matched = 0;
196                while copy_start + matched < n {
197                    let src_idx = ref_pos + matched;
198                    if src_idx >= copy_start {
199                        break; // cannot read beyond pre-existing content
200                    }
201                    if decoded[src_idx] != self.string[copy_start + matched] {
202                        break;
203                    }
204                    decoded.push(decoded[src_idx]);
205                    matched += 1;
206                }
207                if matched == 0 {
208                    return None; // pointer must copy at least one symbol
209                }
210                pointer_count += 1;
211            } else {
212                // v == eos, but we filtered those out above
213                return None;
214            }
215        }
216
217        Some((decoded, active_len, pointer_count))
218    }
219}
220
221impl Problem for MinimumInternalMacroDataCompression {
222    const NAME: &'static str = "MinimumInternalMacroDataCompression";
223    type Solution = Vec<usize>;
224    type Value = Min<i64>;
225
226    crate::problem_parameters![("alphabet_size", alphabet_size), ("string_len", string_len),];
227
228    fn variant() -> Vec<(&'static str, &'static str)> {
229        crate::variant_params![]
230    }
231
232    fn evaluate(
233        &self,
234        config: &Self::Solution,
235    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
236        Ok({
237            let n = self.string.len();
238            if config.len() != n {
239                return Err(crate::traits::EvaluationError::InvalidConfiguration(
240                    "macro encoding length does not match the string".into(),
241                ));
242            }
243
244            // Handle empty string
245            if n == 0 {
246                return Ok(Min(Some(0)));
247            }
248
249            match self.decode(config) {
250                Some((decoded, active_len, pointer_count)) => {
251                    if decoded != self.string {
252                        Min(None)
253                    } else {
254                        let active_len = i64::try_from(active_len).map_err(|_| {
255                            crate::traits::EvaluationError::IntegerOverflow(
256                                "converting the active encoding length to i64".to_string(),
257                            )
258                        })?;
259                        let pointer_count = i64::try_from(pointer_count).map_err(|_| {
260                            crate::traits::EvaluationError::IntegerOverflow(
261                                "converting the pointer count to i64".to_string(),
262                            )
263                        })?;
264                        let pointer_cost = self
265                            .pointer_cost
266                            .checked_sub(1)
267                            .and_then(|cost| cost.checked_mul(pointer_count))
268                            .ok_or_else(|| {
269                                crate::traits::EvaluationError::IntegerOverflow(
270                                    "computing the internal macro pointer cost".to_string(),
271                                )
272                            })?;
273                        let cost = active_len.checked_add(pointer_cost).ok_or_else(|| {
274                            crate::traits::EvaluationError::IntegerOverflow(
275                                "computing the internal macro compression cost".to_string(),
276                            )
277                        })?;
278                        Min(Some(cost))
279                    }
280                }
281                None => Min(None),
282            }
283        })
284    }
285}
286
287impl crate::solvers::BruteForceProblem for MinimumInternalMacroDataCompression {
288    fn dimensions(&self) -> Vec<usize> {
289        let n = self.string.len();
290        let domain = self.alphabet_size + n + 1; // literals + EOS + pointers
291        vec![domain; n]
292    }
293}
294
295crate::declare_variants! {
296    default MinimumInternalMacroDataCompression => "(alphabet_size + string_len + 1) ^ string_len",
297}
298
299crate::register_brute_force! {
300    MinimumInternalMacroDataCompression,
301}
302
303#[cfg(feature = "example-db")]
304pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
305    // Issue #442 example: alphabet {a,b,c} (3), s="abcabcabc" (9), h=2
306    // Optimal: C = [a, b, c, ptr(0), ptr(0), EOS, EOS, EOS, EOS]
307    //   active_len = 5, pointers = 2
308    //   cost = 5 + (2-1)*2 = 7
309    //
310    // Config encoding:
311    //   alphabet_size = 3, string_len = 9, domain = 3+9+1 = 13
312    //   Literals: 0=a, 1=b, 2=c
313    //   EOS: 3
314    //   Pointers: 4=ptr(C[0]), 5=ptr(C[1]), ...
315    let s: Vec<usize> = vec![0, 1, 2, 0, 1, 2, 0, 1, 2];
316    let optimal_config = vec![
317        0, 1, 2, // literals a, b, c
318        4, // ptr(C[0]) -> greedy "abc"
319        4, // ptr(C[0]) -> greedy "abc"
320        3, 3, 3, 3, // EOS padding
321    ];
322    vec![crate::example_db::specs::ModelExampleSpec {
323        id: "minimum_internal_macro_data_compression",
324        instance: Box::new(MinimumInternalMacroDataCompression::new(3, s, 2)),
325        optimal_config: serde_json::to_value(optimal_config)
326            .expect("solution serialization must succeed"),
327        optimal_value: serde_json::json!(7),
328    }]
329}
330
331#[cfg(test)]
332#[path = "../../unit_tests/models/misc/minimum_internal_macro_data_compression.rs"]
333mod tests;