Skip to main content

problemreductions/models/misc/
rectilinear_picture_compression.rs

1//! Rectilinear Picture Compression problem implementation.
2//!
3//! Given an m x n binary matrix M and a nonnegative integer K, determine whether
4//! there exists a collection of at most K axis-aligned all-1 rectangles that
5//! covers precisely the 1-entries of M. Each rectangle (r1, c1, r2, c2) with
6//! r1 <= r2, c1 <= c2 covers entries M[i][j] for r1 <= i <= r2, c1 <= j <= c2,
7//! and every covered entry must be 1.
8
9use crate::registry::{FieldInfo, ProblemSchemaEntry};
10use crate::traits::Problem;
11use serde::de::Deserializer;
12use serde::{Deserialize, Serialize};
13
14type Rectangle = (usize, usize, usize, usize);
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "RectilinearPictureCompression",
19        display_name: "Rectilinear Picture Compression",
20        aliases: &[],
21        dimensions: &[],
22        category: crate::registry::ProblemCategory::Misc,
23        module_path: module_path!(),
24        description: "Cover all 1-entries of a binary matrix with at most K axis-aligned all-1 rectangles",
25        fields: &[
26            FieldInfo { name: "matrix", type_name: "Vec<Vec<bool>>", description: "m x n binary matrix" },
27            FieldInfo { name: "bound", type_name: "i64", description: "Maximum number of rectangles allowed" },
28        ],
29    }
30}
31
32/// The Rectilinear Picture Compression problem.
33///
34/// Given an m x n binary matrix M and a nonnegative integer K, determine whether
35/// there exists a collection of at most K axis-aligned all-1 rectangles that
36/// covers precisely the 1-entries of M.
37///
38/// # Representation
39///
40/// The configuration space consists of the maximal all-1 rectangles in the
41/// matrix. Each variable is binary: 1 if the rectangle is selected, 0 otherwise.
42/// The problem is satisfiable iff the selected rectangles number at most K and
43/// their union covers all 1-entries.
44///
45/// # Example
46///
47/// ```
48/// use problemreductions::models::misc::RectilinearPictureCompression;
49/// use problemreductions::{Problem, BruteForce};
50///
51/// let matrix = vec![
52///     vec![true, true, false, false],
53///     vec![true, true, false, false],
54///     vec![false, false, true, true],
55///     vec![false, false, true, true],
56/// ];
57/// let problem = RectilinearPictureCompression::new(matrix, 2);
58/// let solver = BruteForce::new();
59/// let solution = solver.solve(&problem).unwrap();
60/// assert!(solution.is_some());
61/// ```
62#[derive(Debug, Clone, Serialize)]
63pub struct RectilinearPictureCompression {
64    matrix: Vec<Vec<bool>>,
65    bound: i64,
66    #[serde(skip)]
67    maximal_rects: Vec<Rectangle>,
68}
69
70impl<'de> Deserialize<'de> for RectilinearPictureCompression {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        #[derive(Deserialize)]
76        struct Inner {
77            matrix: Vec<Vec<bool>>,
78            bound: i64,
79        }
80        let inner = Inner::deserialize(deserializer)?;
81        Self::try_new(inner.matrix, inner.bound).map_err(serde::de::Error::custom)
82    }
83}
84
85impl RectilinearPictureCompression {
86    /// Create a new RectilinearPictureCompression instance.
87    ///
88    /// # Panics
89    ///
90    /// Panics if `matrix` is empty or has inconsistent row lengths.
91    pub fn new(matrix: Vec<Vec<bool>>, bound: i64) -> Self {
92        Self::try_new(matrix, bound).unwrap_or_else(|error| panic!("{error}"))
93    }
94
95    fn try_new(
96        matrix: Vec<Vec<bool>>,
97        bound: i64,
98    ) -> Result<Self, crate::registry::ConstructionError> {
99        if matrix.is_empty() {
100            return Err("Matrix must not be empty".into());
101        }
102        let cols = matrix[0].len();
103        if cols == 0 {
104            return Err("Matrix must have at least one column".into());
105        }
106        if matrix.iter().any(|row| row.len() != cols) {
107            return Err("All rows must have the same length".into());
108        }
109        let mut instance = Self {
110            matrix,
111            bound,
112            maximal_rects: Vec::new(),
113        };
114        instance.maximal_rects = instance.compute_maximal_rectangles();
115        Ok(instance)
116    }
117
118    /// Returns the number of rows in the matrix.
119    pub fn num_rows(&self) -> usize {
120        self.matrix.len()
121    }
122
123    /// Returns the number of columns in the matrix.
124    pub fn num_cols(&self) -> usize {
125        self.matrix[0].len()
126    }
127
128    /// Returns the bound K.
129    pub fn bound(&self) -> i64 {
130        self.bound
131    }
132
133    /// Returns a reference to the binary matrix.
134    pub fn matrix(&self) -> &[Vec<bool>] {
135        &self.matrix
136    }
137
138    /// Returns the precomputed maximal all-1 sub-rectangles.
139    ///
140    /// Each rectangle is `(r1, c1, r2, c2)` covering rows `r1..=r2` and
141    /// columns `c1..=c2`.
142    pub fn maximal_rectangles(&self) -> &[Rectangle] {
143        &self.maximal_rects
144    }
145
146    /// Number of cached maximal all-1 rectangles.
147    pub fn num_rectangles(&self) -> usize {
148        self.maximal_rects.len()
149    }
150
151    /// Sum of maximal rectangle areas, counting overlapping cells separately.
152    pub fn total_rectangle_area(&self) -> usize {
153        self.maximal_rects
154            .iter()
155            .map(|&(r1, c1, r2, c2)| (r2 - r1 + 1) * (c2 - c1 + 1))
156            .sum()
157    }
158
159    fn build_prefix_sum(&self) -> Vec<Vec<usize>> {
160        let m = self.num_rows();
161        let n = self.num_cols();
162        let mut prefix_sum = vec![vec![0; n + 1]; m + 1];
163
164        for r in 0..m {
165            let mut row_sum = 0;
166            for c in 0..n {
167                row_sum += usize::from(self.matrix[r][c]);
168                prefix_sum[r + 1][c + 1] = prefix_sum[r][c + 1] + row_sum;
169            }
170        }
171
172        prefix_sum
173    }
174
175    fn range_is_all_ones(
176        prefix_sum: &[Vec<usize>],
177        r1: usize,
178        c1: usize,
179        r2: usize,
180        c2: usize,
181    ) -> bool {
182        let area = (r2 - r1 + 1) * (c2 - c1 + 1);
183        let sum = prefix_sum[r2 + 1][c2 + 1] + prefix_sum[r1][c1]
184            - prefix_sum[r1][c2 + 1]
185            - prefix_sum[r2 + 1][c1];
186        sum == area
187    }
188
189    /// Enumerate all maximal all-1 sub-rectangles in the matrix.
190    ///
191    /// A rectangle is maximal if it cannot be extended one step left, right,
192    /// up, or down while remaining all-1. The result is sorted lexicographically.
193    fn compute_maximal_rectangles(&self) -> Vec<Rectangle> {
194        let m = self.num_rows();
195        let n = self.num_cols();
196
197        // Step 1: Enumerate right-maximal candidate rectangles by fixing
198        // (r1, c1, r2) and taking the widest all-1 prefix common to rows r1..=r2.
199        let mut candidates = Vec::new();
200        for r1 in 0..m {
201            for c1 in 0..n {
202                if !self.matrix[r1][c1] {
203                    continue;
204                }
205                // Find the rightmost column from c1 that is all-1 in row r1.
206                let mut c_max = n;
207                for c in c1..n {
208                    if !self.matrix[r1][c] {
209                        c_max = c;
210                        break;
211                    }
212                }
213                // Extend downward row by row, narrowing column range.
214                let mut c_end = c_max; // exclusive upper bound on columns
215                for r2 in r1..m {
216                    // Narrow c_end based on row r2.
217                    let mut new_c_end = c1;
218                    for c in c1..c_end {
219                        if self.matrix[r2][c] {
220                            new_c_end = c + 1;
221                        } else {
222                            break;
223                        }
224                    }
225                    if new_c_end <= c1 {
226                        break;
227                    }
228                    c_end = new_c_end;
229                    candidates.push((r1, c1, r2, c_end - 1));
230                }
231            }
232        }
233
234        // Step 2: Remove duplicates.
235        candidates.sort();
236        candidates.dedup();
237
238        // Step 3: Filter to keep only rectangles that cannot be extended in
239        // any cardinal direction. A 2D prefix sum makes each extension check O(1).
240        let prefix_sum = self.build_prefix_sum();
241        candidates
242            .into_iter()
243            .filter(|&(r1, c1, r2, c2)| {
244                let can_extend_left =
245                    c1 > 0 && Self::range_is_all_ones(&prefix_sum, r1, c1 - 1, r2, c1 - 1);
246                let can_extend_right =
247                    c2 + 1 < n && Self::range_is_all_ones(&prefix_sum, r1, c2 + 1, r2, c2 + 1);
248                let can_extend_up =
249                    r1 > 0 && Self::range_is_all_ones(&prefix_sum, r1 - 1, c1, r1 - 1, c2);
250                let can_extend_down =
251                    r2 + 1 < m && Self::range_is_all_ones(&prefix_sum, r2 + 1, c1, r2 + 1, c2);
252
253                !(can_extend_left || can_extend_right || can_extend_up || can_extend_down)
254            })
255            .collect()
256    }
257}
258
259impl Problem for RectilinearPictureCompression {
260    const NAME: &'static str = "RectilinearPictureCompression";
261    type Solution = Vec<bool>;
262    type Value = crate::types::Or;
263
264    crate::problem_parameters![
265        ("num_cols", num_cols),
266        ("num_rows", num_rows),
267        ("num_rectangles", num_rectangles),
268        ("total_rectangle_area", total_rectangle_area),
269    ];
270
271    fn variant() -> Vec<(&'static str, &'static str)> {
272        crate::variant_params![]
273    }
274
275    fn evaluate(
276        &self,
277        config: &Self::Solution,
278    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
279        Ok({
280            crate::types::Or({
281                let rects = &self.maximal_rects;
282                if config.len() != rects.len() {
283                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
284                        "rectangle-selection length does not match the maximal rectangles".into(),
285                    ));
286                }
287                // Count selected rectangles.
288                let selected_count = config.iter().filter(|&&selected| selected).count();
289                let selected_count = i64::try_from(selected_count).map_err(|_| {
290                    crate::traits::EvaluationError::IntegerOverflow(
291                        "converting selected-rectangle count to i64".into(),
292                    )
293                })?;
294                if selected_count > self.bound {
295                    return Ok(crate::types::Or(false));
296                }
297
298                // Check that all 1-entries are covered.
299                let m = self.num_rows();
300                let n = self.num_cols();
301                let mut covered = vec![vec![false; n]; m];
302                for (i, &x) in config.iter().enumerate() {
303                    if x {
304                        let (r1, c1, r2, c2) = rects[i];
305                        for row in &mut covered[r1..=r2] {
306                            for cell in &mut row[c1..=c2] {
307                                *cell = true;
308                            }
309                        }
310                    }
311                }
312
313                for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) {
314                    for (&entry, &cov) in row_m.iter().zip(row_c.iter()) {
315                        if entry && !cov {
316                            return Ok(crate::types::Or(false));
317                        }
318                    }
319                }
320
321                true
322            })
323        })
324    }
325}
326
327impl crate::solvers::BruteForceProblem for RectilinearPictureCompression {
328    fn dimensions(&self) -> Vec<usize> {
329        vec![2; self.maximal_rects.len()]
330    }
331}
332
333crate::declare_variants! {
334    default RectilinearPictureCompression => "2^(num_rows * num_cols)",
335}
336
337crate::register_brute_force! {
338    RectilinearPictureCompression decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
339}
340
341#[cfg(feature = "example-db")]
342pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
343    vec![crate::example_db::specs::ModelExampleSpec {
344        id: "rectilinear_picture_compression",
345        // Config: select both maximal rectangles (the two 2x2 blocks).
346        // The maximal rectangles for this matrix are exactly:
347        // (0,0,1,1) and (2,2,3,3), so config [1,1] selects both.
348        instance: Box::new(RectilinearPictureCompression::new(
349            vec![
350                vec![true, true, false, false],
351                vec![true, true, false, false],
352                vec![false, false, true, true],
353                vec![false, false, true, true],
354            ],
355            2,
356        )),
357        optimal_config: serde_json::json!(vec![true, true]),
358        optimal_value: serde_json::json!(true),
359    }]
360}
361
362#[cfg(test)]
363#[path = "../../unit_tests/models/misc/rectilinear_picture_compression.rs"]
364mod tests;